From 2c1e6a159941332d7fc3c21d81008c3294acb3d0 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 22 Jun 2026 09:44:18 -0500 Subject: [PATCH 001/126] chore: add modules to requirements-testing.txt that are needed for testing --- requirements-testing.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements-testing.txt b/requirements-testing.txt index 21bba85648c..aab161ee7f1 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -3,4 +3,6 @@ mypy==1.5.1 hypothesis>=6.131.9,<7 hypothesis-jsonschema==0.19.0 types-requests==2.28.5 -lxml \ No newline at end of file +lxml +coverage +pytest-django From 18c15fe6964a44a5b6d119353ac213bb4a86edfe Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 24 Jun 2026 13:51:48 -0500 Subject: [PATCH 002/126] fix: expose specifyweb.settings to pytest-django --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000000..c5c37d97a12 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +DJANGO_SETTINGS_MODULE = specifyweb.settings From cffe14ee9b151e3c848dc8658464d4f8793c0ba2 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Tue, 7 Jul 2026 14:16:45 -0500 Subject: [PATCH 003/126] added a new test --- .github/workflows/new-test.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/new-test.yaml diff --git a/.github/workflows/new-test.yaml b/.github/workflows/new-test.yaml new file mode 100644 index 00000000000..ec4e7f9c31a --- /dev/null +++ b/.github/workflows/new-test.yaml @@ -0,0 +1,15 @@ +name: Coverage + +on: + push: + workflow_dispatch: + +jobs: + check: + name: Get cpu info + runs-on: ubuntu-latest + steps: + - name: get cpuinfo + id: cpuinfo + run: | + lscpu From de8b58cfa50568b283cfa399f8a91463aead18ba Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Tue, 7 Jul 2026 15:01:31 -0500 Subject: [PATCH 004/126] get meminfo --- .github/workflows/new-test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/new-test.yaml b/.github/workflows/new-test.yaml index ec4e7f9c31a..13aafac9b53 100644 --- a/.github/workflows/new-test.yaml +++ b/.github/workflows/new-test.yaml @@ -13,3 +13,6 @@ jobs: id: cpuinfo run: | lscpu + - name: get meminfo + id: meminfo + run: free -h From c8d1719bfa183dd28671b0c9b174a908016b7a42 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 11:10:47 -0500 Subject: [PATCH 005/126] feat(meta): added a new github action to report on code coverage --- .github/workflows/coverage.yml | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000000..28a5ceae274 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,110 @@ +name: Coverage + +on: + push: + workflow_dispatch: + +jobs: + # copied from test.yml + changes: + name: Check if back-end or front-end files changed + # Don't run this for pushes generated by push.yml on weblate-localization + # branch → prevents an infinite loop of actions triggering actions + # + # Also, "push" event is not triggered for forks, so need to listen for + # pull_requests, but only for external ones, so as not to run the action + # twice + if: | + github.event.head_commit.committer.name != 'Hosted Weblate' && + github.event.head_commit.committer.name != 'github-actions' && + ( + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.fork == true + ) + runs-on: ubuntu-latest + outputs: + backend_changed: ${{ steps.back-end-check.outputs.changed }} + frontend: ${{ steps.filter.outputs.frontend }} + frontend_changes: ${{ steps.filter.outputs.frontend_files }} + + steps: + - uses: actions/checkout@v4 + + - name: Find all changed files + uses: dorny/paths-filter@v3 + id: filter + with: + base: ${{ github.ref }} + list-files: escape + filters: | + frontend: + - 'specifyweb/frontend/**' + backend: + - '**' + + - name: Check if any non-front-end files changed + id: back-end-check + run: | + changed_files=`echo "${{steps.filter.outputs.backend_files}}" | tr " " "\n" | grep -v 'specifyweb/frontend/' || echo ""` + echo "Changed back-end files: ${changed_files}" + echo "changed=$([[ -z "${changed_files}" ]] && echo "" || echo "1")" >> $GITHUB_OUTPUT + + setup: + needs: changes + #if: | + # needs.changes.outputs.backend_changed || + # needs.changes.outputs.frontend_changed + runs-on: ubuntu-latest + + + steps: + - name: Install needed packages + id: apt + run: | + sudo apt update + sudo apt install docker-compose-v2 + + - uses: actions/checkout@v4 + + - name: Start the containers + id: startup + run: docker compose up --build -d + + - name: Fetch coverage script + id: clone-coverage + run: | + eval "$(ssh-agent -s)" + ssh-add <<< '${{ secrets.PRIVATE_SSH_KEY }}' + cd ~ + git clone git@github.com:specify/specify-development + sudo install -m 0777 -o "$USER" specify-development/scripts/tests/{run-coverage,test_coverage.py} /usr/bin/ + cd - + + - name: Configure specify + id: config-specify + run: | + sed -i 's/\(.*\)\(NAME=\).*/\1\2root/; s/\(.*\)\(PASSWORD=\).*/\1\2password/; s/\(.*\) = \(.*\)/\1=\2/' .env + sed -i 's/#[ ]*\(.*requirements-testing.txt\)/\1/; s/\(COPY\) \(requirements-testing.txt\)/\1 --chown=specify:specify \2' Dockerfile + sed -i 's/\(\(DATABASE_\|MASTER_\|MIGRATOR\|APP_USER_\)[^O].*\) = \(.*\)/\1 = os.environ.get("\1", "")/' specifyweb/settings/specify_settings.py + sudo chmod a+x $(find . -type d) + sudo chmod -R a+rw . + docker compose up --build -d + + + run: + needs: setup + + steps: + - name: Run the script + id: run-coverage + run: | + run-coverage -t pj -a pj -d pj -o csv,html + + - name: Upload report as artifact + uses: actions/upload-artifact@v7 + with: + name: coverage-report + path: ./report.tar.bz2 + archive: false + + From 66fe30b5027c50cffdaa0a4e2012158f8068cd27 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 11:43:02 -0500 Subject: [PATCH 006/126] fix: added required `runs-on` parameter --- .github/workflows/coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 28a5ceae274..763c0f27b0d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -93,6 +93,7 @@ jobs: run: needs: setup + runs-on: ubuntu-latest steps: - name: Run the script From 3e44a4760f582c1b96d76a82bb96861b2148fbeb Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 11:52:42 -0500 Subject: [PATCH 007/126] chore: fix if clause of setup job to trigger if a file was changed or we manually ran it --- .github/workflows/coverage.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 763c0f27b0d..af476b2d15a 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -51,9 +51,10 @@ jobs: setup: needs: changes - #if: | - # needs.changes.outputs.backend_changed || - # needs.changes.outputs.frontend_changed + if: | + (needs.changes.outputs.backend_changed || + needs.changes.outputs.frontend_changed) || + github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest From c7337c5b4ae27d041aef6b9a4c8b80bc7a68c0a0 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 12:47:33 -0500 Subject: [PATCH 008/126] fix: added a retention-days field so that the report does not linger --- .github/workflows/coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index af476b2d15a..63cb0d708f7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -108,5 +108,6 @@ jobs: name: coverage-report path: ./report.tar.bz2 archive: false + retention-days: 1 From 344799cb33e996d2f5e43f03daa3d98ae55621f6 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 13:06:11 -0500 Subject: [PATCH 009/126] fix: removed doubly starting the docker container --- .github/workflows/coverage.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 63cb0d708f7..0bce57a6956 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -67,10 +67,6 @@ jobs: - uses: actions/checkout@v4 - - name: Start the containers - id: startup - run: docker compose up --build -d - - name: Fetch coverage script id: clone-coverage run: | From 27c58966407ac7bd43d2fd89cda56ce0dd77ab1f Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 13:08:27 -0500 Subject: [PATCH 010/126] fix: changed secret name --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0bce57a6956..197efd7a195 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -71,7 +71,7 @@ jobs: id: clone-coverage run: | eval "$(ssh-agent -s)" - ssh-add <<< '${{ secrets.PRIVATE_SSH_KEY }}' + ssh-add <<< '${{ secrets.COVERAGE_SCRIPT_CLONE }}' cd ~ git clone git@github.com:specify/specify-development sudo install -m 0777 -o "$USER" specify-development/scripts/tests/{run-coverage,test_coverage.py} /usr/bin/ From 2bfc8aa8eeb3279a4ba49aa67aea4c7220ab0763 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 13:23:30 -0500 Subject: [PATCH 011/126] fix: added dependancy to install list for coverage action --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 197efd7a195..506b55f575e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -63,7 +63,7 @@ jobs: id: apt run: | sudo apt update - sudo apt install docker-compose-v2 + sudo apt install docker-compose-v2 openssh-client - uses: actions/checkout@v4 From e868f94628ad144d477e11a06cc36f2eb904d27d Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 13:40:18 -0500 Subject: [PATCH 012/126] temp: added debugging output --- .github/workflows/coverage.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 506b55f575e..d96d24c47cf 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -72,9 +72,13 @@ jobs: run: | eval "$(ssh-agent -s)" ssh-add <<< '${{ secrets.COVERAGE_SCRIPT_CLONE }}' + echo "ssh key registered" cd ~ + echo "cd-ed home" git clone git@github.com:specify/specify-development + echo "repo cloned" sudo install -m 0777 -o "$USER" specify-development/scripts/tests/{run-coverage,test_coverage.py} /usr/bin/ + echo "installed scripts" cd - - name: Configure specify From bccbd6f35b80496ea2d4d15ee2673cfc67b804bd Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 13:50:42 -0500 Subject: [PATCH 013/126] fix: read ssh key from stdin --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d96d24c47cf..04a1f54877e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -71,7 +71,7 @@ jobs: id: clone-coverage run: | eval "$(ssh-agent -s)" - ssh-add <<< '${{ secrets.COVERAGE_SCRIPT_CLONE }}' + ssh-add - <<< '${{ secrets.COVERAGE_SCRIPT_CLONE }}' echo "ssh key registered" cd ~ echo "cd-ed home" From 16e2a0a4da204ef9491c15072a68db3e9a91ca88 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 13:53:39 -0500 Subject: [PATCH 014/126] chore: remove debugging output added in e868f94628a --- .github/workflows/coverage.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 04a1f54877e..f17185dbb63 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -72,13 +72,9 @@ jobs: run: | eval "$(ssh-agent -s)" ssh-add - <<< '${{ secrets.COVERAGE_SCRIPT_CLONE }}' - echo "ssh key registered" cd ~ - echo "cd-ed home" git clone git@github.com:specify/specify-development - echo "repo cloned" sudo install -m 0777 -o "$USER" specify-development/scripts/tests/{run-coverage,test_coverage.py} /usr/bin/ - echo "installed scripts" cd - - name: Configure specify From c5804acf96f32bd18722f61db4351417250528ee Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 13:55:40 -0500 Subject: [PATCH 015/126] chore: update readme to document new secret key --- .github/workflows/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index ca5de021704..5bdddd51a1f 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -69,5 +69,17 @@ Documentation for used GitHub secrets. + + COVERAGE_SCRIPT_CLONE + Yes + + Github Deploy token linked to the [development repo](github.com/specify/specify-development) to allow actions to clone it. + + + + #8278 + + + From 9ef2fcb1e03d9ba5a8db44a7fa9e3c3c6dee037e Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 15:24:25 -0500 Subject: [PATCH 016/126] fix: terminate sed s-expression --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f17185dbb63..395832fdb24 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -81,7 +81,7 @@ jobs: id: config-specify run: | sed -i 's/\(.*\)\(NAME=\).*/\1\2root/; s/\(.*\)\(PASSWORD=\).*/\1\2password/; s/\(.*\) = \(.*\)/\1=\2/' .env - sed -i 's/#[ ]*\(.*requirements-testing.txt\)/\1/; s/\(COPY\) \(requirements-testing.txt\)/\1 --chown=specify:specify \2' Dockerfile + sed -i 's/#[ ]*\(.*requirements-testing.txt\)/\1/; s/\(COPY\) \(requirements-testing.txt\)/\1 --chown=specify:specify \2/' Dockerfile sed -i 's/\(\(DATABASE_\|MASTER_\|MIGRATOR\|APP_USER_\)[^O].*\) = \(.*\)/\1 = os.environ.get("\1", "")/' specifyweb/settings/specify_settings.py sudo chmod a+x $(find . -type d) sudo chmod -R a+rw . From aa259713b10a8fd8f97c6b951bffd826258aeabb Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 15:30:06 -0500 Subject: [PATCH 017/126] fix: allow discovery of run-coverage --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 395832fdb24..b6a0b92563c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -96,7 +96,7 @@ jobs: - name: Run the script id: run-coverage run: | - run-coverage -t pj -a pj -d pj -o csv,html + PATH="/usr/bin:$PATH" run-coverage -t pj -a pj -d pj -o csv,html - name: Upload report as artifact uses: actions/upload-artifact@v7 From f975f2cb24ccee17c9ddf983592fe1f10fac6feb Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 15:38:09 -0500 Subject: [PATCH 018/126] fix: manually find the path to run-coverage --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b6a0b92563c..395583c8c51 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -96,7 +96,7 @@ jobs: - name: Run the script id: run-coverage run: | - PATH="/usr/bin:$PATH" run-coverage -t pj -a pj -d pj -o csv,html + /usr/bin/run-coverage -t pj -a pj -d pj -o csv,html - name: Upload report as artifact uses: actions/upload-artifact@v7 From e14f0a92efbdeef5917785ee59da668760359b05 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 8 Jul 2026 15:56:20 -0500 Subject: [PATCH 019/126] fix: combine setup and run jobs so that they exist on the same VM --- .github/workflows/coverage.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 395583c8c51..ed32dc17736 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -49,7 +49,7 @@ jobs: echo "Changed back-end files: ${changed_files}" echo "changed=$([[ -z "${changed_files}" ]] && echo "" || echo "1")" >> $GITHUB_OUTPUT - setup: + setup-and-run: needs: changes if: | (needs.changes.outputs.backend_changed || @@ -87,16 +87,10 @@ jobs: sudo chmod -R a+rw . docker compose up --build -d - - run: - needs: setup - runs-on: ubuntu-latest - - steps: - name: Run the script id: run-coverage run: | - /usr/bin/run-coverage -t pj -a pj -d pj -o csv,html + PATH="/usr/bin:$PATH" run-coverage -t pj -a pj -d pj -o csv,html - name: Upload report as artifact uses: actions/upload-artifact@v7 From 9a6f0bb11efcb4054b39b4190c36a18c3cd67620 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Thu, 9 Jul 2026 12:41:03 -0500 Subject: [PATCH 020/126] fix: dont change database name when configuring specify --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ed32dc17736..504b3c60548 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -80,7 +80,7 @@ jobs: - name: Configure specify id: config-specify run: | - sed -i 's/\(.*\)\(NAME=\).*/\1\2root/; s/\(.*\)\(PASSWORD=\).*/\1\2password/; s/\(.*\) = \(.*\)/\1=\2/' .env + sed -i '/DATABASE_NAME/!s/\(.*\)\(NAME=\).*/\1\2root/; s/\(.*\)\(PASSWORD=\).*/\1\2password/; s/\(.*\) = \(.*\)/\1=\2/' .env sed -i 's/#[ ]*\(.*requirements-testing.txt\)/\1/; s/\(COPY\) \(requirements-testing.txt\)/\1 --chown=specify:specify \2/' Dockerfile sed -i 's/\(\(DATABASE_\|MASTER_\|MIGRATOR\|APP_USER_\)[^O].*\) = \(.*\)/\1 = os.environ.get("\1", "")/' specifyweb/settings/specify_settings.py sudo chmod a+x $(find . -type d) From fd96f57ed06b90736aef67b9f65e34d1e67d08b6 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Fri, 10 Jul 2026 09:21:16 -0500 Subject: [PATCH 021/126] chore: view logs of containers --- .github/workflows/coverage.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 504b3c60548..69e9091b076 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -90,7 +90,9 @@ jobs: - name: Run the script id: run-coverage run: | - PATH="/usr/bin:$PATH" run-coverage -t pj -a pj -d pj -o csv,html + PATH="/usr/bin:$PATH" run-coverage -t p + docker logs mariadb + docker logs specify7 - name: Upload report as artifact uses: actions/upload-artifact@v7 From ec71760c6d32e0f433d174944506ad6261208a77 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Fri, 10 Jul 2026 14:45:41 -0500 Subject: [PATCH 022/126] chore: debugging why mariadb is not accessable --- .github/workflows/coverage.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 69e9091b076..cf91b5bc19e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -90,9 +90,11 @@ jobs: - name: Run the script id: run-coverage run: | - PATH="/usr/bin:$PATH" run-coverage -t p + sleep 100 docker logs mariadb + echo "=================================================================================" docker logs specify7 + docker exec mariadb mariadb -uroot -ppassword -e 'select * from mysql.user where User = 'root';' - name: Upload report as artifact uses: actions/upload-artifact@v7 From e55e91f842a1a6a7aa3d13f2751401e64230c57c Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Fri, 10 Jul 2026 15:00:50 -0500 Subject: [PATCH 023/126] chore: moving docker exec to the top --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index cf91b5bc19e..f3f04d196d6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -91,10 +91,10 @@ jobs: id: run-coverage run: | sleep 100 + docker exec mariadb mariadb -uroot -ppassword -e 'select * from mysql.user where User = 'root';' docker logs mariadb echo "=================================================================================" docker logs specify7 - docker exec mariadb mariadb -uroot -ppassword -e 'select * from mysql.user where User = 'root';' - name: Upload report as artifact uses: actions/upload-artifact@v7 From cd6df3053f217da61606da6719113ceeb80d649b Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Fri, 10 Jul 2026 16:00:11 -0500 Subject: [PATCH 024/126] chore: check out docker ps --- .github/workflows/coverage.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f3f04d196d6..d25acdb55e7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -83,15 +83,17 @@ jobs: sed -i '/DATABASE_NAME/!s/\(.*\)\(NAME=\).*/\1\2root/; s/\(.*\)\(PASSWORD=\).*/\1\2password/; s/\(.*\) = \(.*\)/\1=\2/' .env sed -i 's/#[ ]*\(.*requirements-testing.txt\)/\1/; s/\(COPY\) \(requirements-testing.txt\)/\1 --chown=specify:specify \2/' Dockerfile sed -i 's/\(\(DATABASE_\|MASTER_\|MIGRATOR\|APP_USER_\)[^O].*\) = \(.*\)/\1 = os.environ.get("\1", "")/' specifyweb/settings/specify_settings.py - sudo chmod a+x $(find . -type d) - sudo chmod -R a+rw . + for i in range {0..3}; do + sudo chmod a+x $(find . -type d) + sudo chmod -R a+rw . + done docker compose up --build -d - name: Run the script id: run-coverage run: | - sleep 100 - docker exec mariadb mariadb -uroot -ppassword -e 'select * from mysql.user where User = 'root';' + docker exec mariadb mariadb -uroot -ppassword -e 'select * from mysql.user where User = 'root';' || echo Mariadb crashed! + docker ps docker logs mariadb echo "=================================================================================" docker logs specify7 From 65ea138169d7647b7fe16fe18e42c400081f3314 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Fri, 10 Jul 2026 16:28:26 -0500 Subject: [PATCH 025/126] chore: view stats --- .github/workflows/coverage.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d25acdb55e7..2c81b911e0d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -88,15 +88,16 @@ jobs: sudo chmod -R a+rw . done docker compose up --build -d + docker stats --no-stream - name: Run the script id: run-coverage run: | docker exec mariadb mariadb -uroot -ppassword -e 'select * from mysql.user where User = 'root';' || echo Mariadb crashed! - docker ps docker logs mariadb echo "=================================================================================" docker logs specify7 + docker ps - name: Upload report as artifact uses: actions/upload-artifact@v7 From 578b12873e553ac9570c4c91b450d6c2eba85d9e Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:05:37 -0500 Subject: [PATCH 026/126] fix(trees): sync tree conformation with URL cache --- .../js_src/lib/components/TreeView/index.tsx | 32 ++++++++++++++----- .../frontend/js_src/lib/utils/cache/index.ts | 17 ++++++---- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/index.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/index.tsx index da6540bf965..e444aa25f05 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/index.tsx @@ -142,15 +142,31 @@ function TreeView({ `conformations${tableName}` ); - React.useEffect( - () => setUrlConformation(serializeConformation(conformation)), - [conformation, setUrlConformation] - ); + // On initial mount, restore conformation from URL or cache. + // The URL is the single source of truth during a session; + // the cache persists state for when the user closes and reopens the tab. + const isFirstRender = React.useRef(true); + React.useEffect(() => { + if (!isFirstRender.current) return; + isFirstRender.current = false; + + if (typeof urlConformation === 'string') { + // URL has a conformation → use it as the initial state + const parsed = deserializeConformation(urlConformation); + setConformation(parsed); + } else if (conformation !== defaultConformation) { + // No URL conformation but cache has one → promote to URL + setUrlConformation(serializeConformation(conformation)); + } + // If neither URL nor cache has a conformation, use the default (empty). + }, [conformation, urlConformation, setConformation, setUrlConformation]); + + // When the user interacts with the tree, persist the conformation + // to both the URL (primary) and the cache (for session restoration). React.useEffect(() => { - if (typeof urlConformation !== 'string') return; - const parsed = deserializeConformation(urlConformation); - setConformation(parsed); - }, [setConformation]); + if (isFirstRender.current) return; + setUrlConformation(serializeConformation(conformation)); + }, [conformation, setUrlConformation]); useTitle(treeText.treeViewTitle({ treeName: table.label })); diff --git a/specifyweb/frontend/js_src/lib/utils/cache/index.ts b/specifyweb/frontend/js_src/lib/utils/cache/index.ts index 86778905fe6..c340a589d0c 100644 --- a/specifyweb/frontend/js_src/lib/utils/cache/index.ts +++ b/specifyweb/frontend/js_src/lib/utils/cache/index.ts @@ -65,7 +65,10 @@ function initialize(): void { */ const parsedValue = JSON.parse(newValue); const [category, key] = parsedKey; - genericSet(category, key, parsedValue); + // Update in-memory cache and fire change event so React hooks + // in this tab update, but skip the localStorage write since + // the originating tab already committed the value. + genericSet(category, key, parsedValue, true, true); } ); eventListenerIsInitialized = true; @@ -139,7 +142,8 @@ function genericSet( key: string, // Any serializable value value: T, - triggerChange = true + triggerChange = true, + skipStorageWrite = false ): T { if (!eventListenerIsInitialized) initialize(); @@ -155,10 +159,11 @@ function genericSet( cache[formattedKey] = value; - globalThis.localStorage.setItem( - formatCacheKey(category, key), - JSON.stringify(value) - ); + if (!skipStorageWrite) + globalThis.localStorage.setItem( + formatCacheKey(category, key), + JSON.stringify(value) + ); if (triggerChange) cacheEvents.trigger('change', { category, key }); From b3d8b9b9efd64cf6f0f048af90377c72ccd5c23a Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:14:03 -0500 Subject: [PATCH 027/126] fix(trees): tree view initial sync --- .../frontend/js_src/lib/components/TreeView/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/index.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/index.tsx index e444aa25f05..c4a8d1a8cbd 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/index.tsx @@ -148,7 +148,6 @@ function TreeView({ const isFirstRender = React.useRef(true); React.useEffect(() => { if (!isFirstRender.current) return; - isFirstRender.current = false; if (typeof urlConformation === 'string') { // URL has a conformation → use it as the initial state @@ -164,7 +163,10 @@ function TreeView({ // When the user interacts with the tree, persist the conformation // to both the URL (primary) and the cache (for session restoration). React.useEffect(() => { - if (isFirstRender.current) return; + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } setUrlConformation(serializeConformation(conformation)); }, [conformation, setUrlConformation]); From 2ff4a344fa4a9abc02275e1af64d2450b457989e Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 13:38:35 +0200 Subject: [PATCH 028/126] Script: "Add Django EOL checker script (fail if <180 days to EOL)" Fixes #8063 --- scripts/check_django_eol.py | 84 +++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 scripts/check_django_eol.py diff --git a/scripts/check_django_eol.py b/scripts/check_django_eol.py new file mode 100644 index 00000000000..92216dfcb71 --- /dev/null +++ b/scripts/check_django_eol.py @@ -0,0 +1,84 @@ +import requests +import datetime +import sys +import subprocess + +THRESHOLD_DAYS = 180 + + +def get_django_version(): + result = subprocess.run( + [sys.executable, "-m", "django", "--version"], + capture_output=True, + text=True + ) + + if result.returncode != 0: + print("ERROR: Unable to determine Django version") + sys.exit(1) + + full_version = result.stdout.strip() + + # Example: + # 4.2.16 -> 4.2 + cycle = ".".join(full_version.split(".")[:2]) + + return full_version, cycle + + +def get_django_eol(cycle): + url = "https://endoflife.date/api/django.json" + + response = requests.get(url) + + if response.status_code != 200: + print("ERROR: Failed to fetch EOL data") + sys.exit(1) + + data = response.json() + + for release in data: + if release["cycle"] == cycle: + return release["eol"] + + print(f"ERROR: No EOL data found for Django {cycle}") + sys.exit(1) + + +def calculate_days_remaining(eol_date): + today = datetime.date.today() + eol = datetime.datetime.strptime( + eol_date, + "%Y-%m-%d" + ).date() + + delta = eol - today + + return delta.days + + +def main(): + full_version, cycle = get_django_version() + + print(f"Detected Django version: {full_version}") + + eol_date = get_django_eol(cycle) + + print(f"Django {cycle} EOL date: {eol_date}") + + days_remaining = calculate_days_remaining(eol_date) + + print(f"Days remaining before EOL: {days_remaining}") + + if days_remaining < THRESHOLD_DAYS: + print( + f"ERROR: Django {cycle} has less than " + f"{THRESHOLD_DAYS} days before EOL" + ) + sys.exit(1) + + print("SUCCESS: Django version is within support window") + + +if __name__ == "__main__": + main() \ No newline at end of file From 629a4f64595ff40f938d235f2ae7cb6b6e348ff5 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 13:50:52 +0200 Subject: [PATCH 029/126] Github: Add github CI for EOL dependencies --- .github/workflows/test.yml | 50 ++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8565522320d..1ec7342aa2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,16 +69,14 @@ jobs: MIGRATOR_NAME: MasterUser MIGRATOR_PASSWORD: MasterPassword APP_USER_NAME: MasterUser - APP_USER_PASSWORD: MasterPassword - options: - --health-cmd="mariadb-admin ping" --health-interval=5s + APP_USER_PASSWORD: MasterPassword + options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 redis: image: redis:latest ports: - 6379 - steps: - uses: actions/checkout@v4 @@ -98,8 +96,7 @@ jobs: - name: Patch Specify datamodel (Sam, you made the Attachment.origFilename too long) - run: - sed -i 's/name="origFilename" + run: sed -i 's/name="origFilename" type="java.lang.String"/name="origFilename" type="text"/' ./Specify6/config/specify_datamodel.xml @@ -135,10 +132,9 @@ jobs: echo "APP_USER_PASSWORD = 'MasterPassword'" >> specifyweb/settings/local_specify_settings.py echo "REDIS_HOST = '127.0.0.1'" >> specifyweb/settings/local_specify_settings.py echo "REDIS_PORT = ${{ job.services.redis.ports[6379] }}" >> specifyweb/settings/local_specify_settings.py - + - name: Need these files to be present - run: - make specifyweb/settings/build_version.py + run: make specifyweb/settings/build_version.py specifyweb/settings/secret_key.py - name: Verify MariaDB connection @@ -148,7 +144,7 @@ jobs: while ! mysqladmin ping -h"127.0.0.1" -P"$PORT" --silent; do sleep 1 done - + - name: Run Mypy type checker run: VIRTUAL_ENV=./ve make typecheck @@ -180,7 +176,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version-file: specifyweb/frontend/js_src/package.json - cache: 'npm' + cache: "npm" cache-dependency-path: specifyweb/frontend/js_src/package-lock.json - name: Build frontend @@ -199,8 +195,7 @@ jobs: - name: Clone branch that stores localization strings # Listen for changes to production branch - if: - github.ref == format('refs/heads/{0}', + if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) id: weblate uses: actions/checkout@v3 @@ -215,8 +210,7 @@ jobs: npm run localizationTests -- --emit ../../../weblate-localization/strings - name: Sync localization strings with Weblate (only on production branch) - if: - github.ref == format('refs/heads/{0}', + if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) working-directory: weblate-localization run: | @@ -282,8 +276,7 @@ jobs: # - This would have already run on the feature branch by the time code # is pushed into production. # - There shouldn't be many direct pushes to production anyway - if: - github.ref != format('refs/heads/{0}', + if: github.ref != format('refs/heads/{0}', github.event.repository.default_branch) working-directory: specifyweb/frontend/js_src run: | @@ -322,4 +315,25 @@ jobs: -m 'Lint code with ESLint and Prettier' \ -m "Triggered by ${{ github.sha }} on branch ${{ github.ref }}" git push - fi \ No newline at end of file + fi + + eol-check: + name: Check dependency EOL (Django) + runs-on: ubuntu-latest + continue-on-error: true + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + pip install requests django + + - name: Run EOL check + run: | + python scripts/check_django_eol.py From 99345e438465af371786745bed1d23643781e98a Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 14:31:16 +0200 Subject: [PATCH 030/126] Fix: Improve run time out handling --- scripts/check_django_eol.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/check_django_eol.py b/scripts/check_django_eol.py index 92216dfcb71..90c3ff84cbd 100644 --- a/scripts/check_django_eol.py +++ b/scripts/check_django_eol.py @@ -2,6 +2,8 @@ import datetime import sys import subprocess +import re +from requests import RequestException THRESHOLD_DAYS = 180 @@ -21,7 +23,11 @@ def get_django_version(): # Example: # 4.2.16 -> 4.2 - cycle = ".".join(full_version.split(".")[:2]) + m = re.match(r"^(\d+)\.(\d+)", full_version) + if not m: + print(f"ERROR: Unexpected Django version format: {full_version}") + sys.exit(1) + cycle = f"{m.group(1)}.{m.group(2)}" return full_version, cycle @@ -29,10 +35,11 @@ def get_django_version(): def get_django_eol(cycle): url = "https://endoflife.date/api/django.json" - response = requests.get(url) - - if response.status_code != 200: - print("ERROR: Failed to fetch EOL data") + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + except RequestException as exc: + print(f"ERROR: Failed to fetch EOL data: {exc}") sys.exit(1) data = response.json() From 8fff9eced15e8bb57f745ba749b6642feba8ffcc Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 15:10:52 +0200 Subject: [PATCH 031/126] Test: Create pr comments when warning triggered --- .github/workflows/test.yml | 43 ++++++++++++++++++++++++++++++++++- scripts/check_django_eol.py | 45 +++++++++++++++++++------------------ 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ec7342aa2f..d96f8b9b668 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -322,6 +322,10 @@ jobs: runs-on: ubuntu-latest continue-on-error: true + permissions: + pull-requests: write + contents: read + steps: - uses: actions/checkout@v4 @@ -335,5 +339,42 @@ jobs: pip install requests django - name: Run EOL check + id: eol + run: | + output=$(python scripts/check_django_eol.py || true) + + echo "$output" + + echo "output<> $GITHUB_OUTPUT + echo "$output" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Parse EOL status + id: parse run: | - python scripts/check_django_eol.py + STATUS=$(echo "${{ steps.eol.outputs.output }}" | grep STATUS= | cut -d= -f2) + echo "status=$STATUS" >> $GITHUB_OUTPUT + + - name: Comment on PR if EOL warning + if: github.event_name == 'pull_request' && steps.parse.outputs.status == 'WARNING' + uses: actions/github-script@v7 + with: + script: | + const output = `${{ steps.eol.outputs.output }}`; + + const body = ` + + \`\`\` + ${output} + \`\`\` + + This dependency is approaching or past End-of-Life. + Please plan an upgrade. + `; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }); \ No newline at end of file diff --git a/scripts/check_django_eol.py b/scripts/check_django_eol.py index 90c3ff84cbd..353a16c308f 100644 --- a/scripts/check_django_eol.py +++ b/scripts/check_django_eol.py @@ -27,6 +27,7 @@ def get_django_version(): if not m: print(f"ERROR: Unexpected Django version format: {full_version}") sys.exit(1) + cycle = f"{m.group(1)}.{m.group(2)}" return full_version, cycle @@ -54,37 +55,37 @@ def get_django_eol(cycle): def calculate_days_remaining(eol_date): today = datetime.date.today() - eol = datetime.datetime.strptime( - eol_date, - "%Y-%m-%d" - ).date() - - delta = eol - today - - return delta.days + eol = datetime.datetime.strptime(eol_date, "%Y-%m-%d").date() + return (eol - today).days def main(): full_version, cycle = get_django_version() - print(f"Detected Django version: {full_version}") - eol_date = get_django_eol(cycle) - - print(f"Django {cycle} EOL date: {eol_date}") - days_remaining = calculate_days_remaining(eol_date) - print(f"Days remaining before EOL: {days_remaining}") - + status = "OK" if days_remaining < THRESHOLD_DAYS: - print( - f"ERROR: Django {cycle} has less than " - f"{THRESHOLD_DAYS} days before EOL" - ) - sys.exit(1) - - print("SUCCESS: Django version is within support window") + status = "WARNING" + + # ---- structured output for GitHub Actions ---- + print(f"STATUS={status}") + print(f"DJANGO_VERSION={full_version}") + print(f"DJANGO_CYCLE={cycle}") + print(f"EOL_DATE={eol_date}") + print(f"DAYS_REMAINING={days_remaining}") + + # ---- human-readable log ---- + print("\n--- Django EOL Report ---") + print(f"Version: {full_version}") + print(f"Cycle: {cycle}") + print(f"EOL date: {eol_date}") + print(f"Days remaining: {days_remaining}") + print(f"Status: {status}") + + # keep non-blocking behavior (CI should NOT fail) + sys.exit(0) if __name__ == "__main__": From fa482ef59baa9b9c90011370d1444f3b209f0bc4 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 15:20:43 +0200 Subject: [PATCH 032/126] Fix: Fix installation --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d96f8b9b668..817d898c5fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -336,7 +336,8 @@ jobs: - name: Install dependencies run: | - pip install requests django + pip install requests + pip install -r requirements.txt - name: Run EOL check id: eol From 8115889ad38b1d53acfe73324e20b55a443bf1e2 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 15:24:05 +0200 Subject: [PATCH 033/126] Fix: Add timeout --- scripts/check_django_eol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check_django_eol.py b/scripts/check_django_eol.py index 353a16c308f..30dc90ea48b 100644 --- a/scripts/check_django_eol.py +++ b/scripts/check_django_eol.py @@ -13,6 +13,7 @@ def get_django_version(): [sys.executable, "-m", "django", "--version"], capture_output=True, text=True + timeout=30 ) if result.returncode != 0: From 6bd897e8750bc626997f3a26587acb70a3238db8 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 18:02:28 +0200 Subject: [PATCH 034/126] Fix: Add timeout to CI --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 817d898c5fa..777c004d40a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -321,6 +321,7 @@ jobs: name: Check dependency EOL (Django) runs-on: ubuntu-latest continue-on-error: true + timeout-minutes: 10 permissions: pull-requests: write From 608776bc859cf83e3104fd56c7d8c4e35c470e29 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 18:08:56 +0200 Subject: [PATCH 035/126] Fix: Alignement --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 777c004d40a..99bef27a191 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -366,17 +366,17 @@ jobs: const body = ` - \`\`\` - ${output} - \`\`\` + \`\`\` + ${output} + \`\`\` - This dependency is approaching or past End-of-Life. - Please plan an upgrade. - `; + This dependency is approaching or past End-of-Life. + Please plan an upgrade. + `; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body - }); \ No newline at end of file + }); From e0773af94902d643f2fa94b63be84d46c1316807 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 18:14:40 +0200 Subject: [PATCH 036/126] Fix: Handle backticks --- .github/workflows/test.yml | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99bef27a191..bd184c5a920 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -360,23 +360,22 @@ jobs: - name: Comment on PR if EOL warning if: github.event_name == 'pull_request' && steps.parse.outputs.status == 'WARNING' uses: actions/github-script@v7 + env: + EOL_OUTPUT: ${{ steps.eol.outputs.output }} with: - script: | - const output = `${{ steps.eol.outputs.output }}`; - - const body = ` - - \`\`\` - ${output} - \`\`\` - - This dependency is approaching or past End-of-Life. - Please plan an upgrade. - `; - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body - }); + script: | + const output = process.env.EOL_OUTPUT || ''; + + const body = + '```\n' + + output.replace(/```/g, '``\\`') + + '\n```\n\n' + + 'This dependency is approaching or past End-of-Life.\n' + + 'Please plan an upgrade.'; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }); From 0a734d296a29c8807bcc6f23413ad2b76cc31371 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 18:57:35 +0200 Subject: [PATCH 037/126] Comment --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bd184c5a920..2a3662625a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -363,6 +363,7 @@ jobs: env: EOL_OUTPUT: ${{ steps.eol.outputs.output }} with: + # Content of pr comment script: | const output = process.env.EOL_OUTPUT || ''; From e8c0147cdb0bc6f342e834799720ddc1ccaecffb Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 19:44:03 +0200 Subject: [PATCH 038/126] Clean --- .github/workflows/test.yml | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a3662625a8..4d63b9ee991 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -363,20 +363,19 @@ jobs: env: EOL_OUTPUT: ${{ steps.eol.outputs.output }} with: - # Content of pr comment - script: | - const output = process.env.EOL_OUTPUT || ''; - - const body = - '```\n' + - output.replace(/```/g, '``\\`') + - '\n```\n\n' + - 'This dependency is approaching or past End-of-Life.\n' + - 'Please plan an upgrade.'; - - await github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body - }); + script: | + const output = process.env.EOL_OUTPUT || ''; + + const body = + '```\n' + + output.replace(/```/g, '``\\`') + + '\n```\n\n' + + 'This dependency is approaching or past End-of-Life.\n' + + 'Please plan an upgrade.'; + + await github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body + }); From 8eaafc3ade5d89a39ec8a73fa21239cacea9056f Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 20:01:58 +0200 Subject: [PATCH 039/126] Fix: test new install for dependecies --- .github/workflows/test.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d63b9ee991..d778874978f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -335,8 +335,17 @@ jobs: with: python-version: "3.11" - - name: Install dependencies + - name: Install system dependencies run: | + sudo apt-get update + sudo apt-get install -y \ + libldap2-dev \ + libsasl2-dev \ + libssl-dev + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip pip install requests pip install -r requirements.txt @@ -347,9 +356,11 @@ jobs: echo "$output" - echo "output<> $GITHUB_OUTPUT - echo "$output" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + { + echo "output<> "$GITHUB_OUTPUT" - name: Parse EOL status id: parse @@ -367,7 +378,7 @@ jobs: const output = process.env.EOL_OUTPUT || ''; const body = - '```\n' + + '```text\n' + output.replace(/```/g, '``\\`') + '\n```\n\n' + 'This dependency is approaching or past End-of-Life.\n' + From eacface43aba3513d507a167dd02d6188d047750 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 20:07:38 +0200 Subject: [PATCH 040/126] Fix: Typo --- scripts/check_django_eol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_django_eol.py b/scripts/check_django_eol.py index 30dc90ea48b..271edede3fe 100644 --- a/scripts/check_django_eol.py +++ b/scripts/check_django_eol.py @@ -12,7 +12,7 @@ def get_django_version(): result = subprocess.run( [sys.executable, "-m", "django", "--version"], capture_output=True, - text=True + text=True, timeout=30 ) From 84328c90b71227d1a7debee51aa671fd2634ddb0 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 20:24:26 +0200 Subject: [PATCH 041/126] Fix: Create or Update github action comment --- .github/workflows/test.yml | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d778874978f..4c198ce95b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -377,16 +377,39 @@ jobs: script: | const output = process.env.EOL_OUTPUT || ''; + const marker = ''; + const body = + marker + '\n' + '```text\n' + output.replace(/```/g, '``\\`') + '\n```\n\n' + 'This dependency is approaching or past End-of-Life.\n' + 'Please plan an upgrade.'; - await github.rest.issues.createComment({ - issue_number: context.issue.number, + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - body + issue_number: context.issue.number, }); + + const existingComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes(marker) + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } From af0d0e940705068d9f385c58cd5280768fa66de2 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Mon, 11 May 2026 20:33:40 +0200 Subject: [PATCH 042/126] Test --- scripts/check_django_eol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check_django_eol.py b/scripts/check_django_eol.py index 271edede3fe..a738d6bf6ec 100644 --- a/scripts/check_django_eol.py +++ b/scripts/check_django_eol.py @@ -7,6 +7,7 @@ THRESHOLD_DAYS = 180 +#Test comment def get_django_version(): result = subprocess.run( From c9544b3c1958c088312a3b9ba9cc3584cfc1b69a Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 10:08:56 +0200 Subject: [PATCH 043/126] Feat: Generalize EOL script --- .github/workflows/test.yml | 24 ++++-- scripts/check_django_eol.py | 94 ---------------------- scripts/check_eol.py | 152 ++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 100 deletions(-) delete mode 100644 scripts/check_django_eol.py create mode 100644 scripts/check_eol.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c198ce95b3..cdf5f47a044 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -318,11 +318,17 @@ jobs: fi eol-check: - name: Check dependency EOL (Django) + name: Check dependency EOL runs-on: ubuntu-latest continue-on-error: true timeout-minutes: 10 + strategy: + matrix: + product: + - django + - python + permissions: pull-requests: write contents: read @@ -336,6 +342,7 @@ jobs: python-version: "3.11" - name: Install system dependencies + if: matrix.product == 'django' run: | sudo apt-get update sudo apt-get install -y \ @@ -347,12 +354,15 @@ jobs: run: | python -m pip install --upgrade pip pip install requests - pip install -r requirements.txt + + if [ "${{ matrix.product }}" = "django" ]; then + pip install -r requirements.txt + fi - name: Run EOL check id: eol run: | - output=$(python scripts/check_django_eol.py || true) + output=$(python scripts/check_eol.py "${{ matrix.product }}" || true) echo "$output" @@ -366,25 +376,27 @@ jobs: id: parse run: | STATUS=$(echo "${{ steps.eol.outputs.output }}" | grep STATUS= | cut -d= -f2) - echo "status=$STATUS" >> $GITHUB_OUTPUT + echo "status=$STATUS" >> "$GITHUB_OUTPUT" - name: Comment on PR if EOL warning if: github.event_name == 'pull_request' && steps.parse.outputs.status == 'WARNING' uses: actions/github-script@v7 env: EOL_OUTPUT: ${{ steps.eol.outputs.output }} + PRODUCT: ${{ matrix.product }} with: script: | const output = process.env.EOL_OUTPUT || ''; + const product = process.env.PRODUCT || 'dependency'; - const marker = ''; + const marker = ``; const body = marker + '\n' + '```text\n' + output.replace(/```/g, '``\\`') + '\n```\n\n' + - 'This dependency is approaching or past End-of-Life.\n' + + `This ${product} version is approaching or past End-of-Life.\n` + 'Please plan an upgrade.'; const { data: comments } = await github.rest.issues.listComments({ diff --git a/scripts/check_django_eol.py b/scripts/check_django_eol.py deleted file mode 100644 index a738d6bf6ec..00000000000 --- a/scripts/check_django_eol.py +++ /dev/null @@ -1,94 +0,0 @@ -import requests -import datetime -import sys -import subprocess -import re -from requests import RequestException - -THRESHOLD_DAYS = 180 - -#Test comment - -def get_django_version(): - result = subprocess.run( - [sys.executable, "-m", "django", "--version"], - capture_output=True, - text=True, - timeout=30 - ) - - if result.returncode != 0: - print("ERROR: Unable to determine Django version") - sys.exit(1) - - full_version = result.stdout.strip() - - # Example: - # 4.2.16 -> 4.2 - m = re.match(r"^(\d+)\.(\d+)", full_version) - if not m: - print(f"ERROR: Unexpected Django version format: {full_version}") - sys.exit(1) - - cycle = f"{m.group(1)}.{m.group(2)}" - - return full_version, cycle - - -def get_django_eol(cycle): - url = "https://endoflife.date/api/django.json" - - try: - response = requests.get(url, timeout=10) - response.raise_for_status() - except RequestException as exc: - print(f"ERROR: Failed to fetch EOL data: {exc}") - sys.exit(1) - - data = response.json() - - for release in data: - if release["cycle"] == cycle: - return release["eol"] - - print(f"ERROR: No EOL data found for Django {cycle}") - sys.exit(1) - - -def calculate_days_remaining(eol_date): - today = datetime.date.today() - eol = datetime.datetime.strptime(eol_date, "%Y-%m-%d").date() - return (eol - today).days - - -def main(): - full_version, cycle = get_django_version() - - eol_date = get_django_eol(cycle) - days_remaining = calculate_days_remaining(eol_date) - - status = "OK" - if days_remaining < THRESHOLD_DAYS: - status = "WARNING" - - # ---- structured output for GitHub Actions ---- - print(f"STATUS={status}") - print(f"DJANGO_VERSION={full_version}") - print(f"DJANGO_CYCLE={cycle}") - print(f"EOL_DATE={eol_date}") - print(f"DAYS_REMAINING={days_remaining}") - - # ---- human-readable log ---- - print("\n--- Django EOL Report ---") - print(f"Version: {full_version}") - print(f"Cycle: {cycle}") - print(f"EOL date: {eol_date}") - print(f"Days remaining: {days_remaining}") - print(f"Status: {status}") - - # keep non-blocking behavior (CI should NOT fail) - sys.exit(0) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/check_eol.py b/scripts/check_eol.py new file mode 100644 index 00000000000..d7ccb5b914b --- /dev/null +++ b/scripts/check_eol.py @@ -0,0 +1,152 @@ +import requests +import datetime +import sys +import subprocess +import re +from requests import RequestException + +THRESHOLD_DAYS = 180 + +SUPPORTED_PRODUCTS = { + "django": { + "api": "https://endoflife.date/api/django.json", + "version_command": [sys.executable, "-m", "django", "--version"], + "version_pattern": r"^(\d+)\.(\d+)", + "display_name": "Django", + }, + "python": { + "api": "https://endoflife.date/api/python.json", + "version_command": [sys.executable, "--version"], + "version_pattern": r"^Python (\d+)\.(\d+)", + "display_name": "Python", + }, +} + + +def get_product_config(product): + config = SUPPORTED_PRODUCTS.get(product) + + if not config: + supported = ", ".join(sorted(SUPPORTED_PRODUCTS.keys())) + print( + f"ERROR: Unsupported product '{product}'. " + f"Supported products: {supported}" + ) + sys.exit(1) + + return config + + +def get_version_info(config): + try: + result = subprocess.run( + config["version_command"], + capture_output=True, + text=True, + timeout=30 + ) + except subprocess.TimeoutExpired: + print("ERROR: Version command timed out") + sys.exit(1) + + if result.returncode != 0: + print("ERROR: Unable to determine version") + sys.exit(1) + + version_output = result.stdout.strip() + + m = re.match(config["version_pattern"], version_output) + + if not m: + print(f"ERROR: Unexpected version format: {version_output}") + sys.exit(1) + + cycle = f"{m.group(1)}.{m.group(2)}" + + if config["display_name"] == "Python": + full_version = version_output.replace("Python ", "") + else: + full_version = version_output + + return full_version, cycle + + +def get_eol_date(config, cycle): + try: + response = requests.get(config["api"], timeout=10) + response.raise_for_status() + except RequestException as exc: + print(f"ERROR: Failed to fetch EOL data: {exc}") + sys.exit(1) + + data = response.json() + + for release in data: + if release["cycle"] == cycle: + return release["eol"] + + print( + f"ERROR: No EOL data found for " + f"{config['display_name']} {cycle}" + ) + sys.exit(1) + + +def calculate_days_remaining(eol_date): + today = datetime.date.today() + + eol = datetime.datetime.strptime( + eol_date, + "%Y-%m-%d" + ).date() + + return (eol - today).days + + +def main(): + if len(sys.argv) != 2: + supported = ", ".join(sorted(SUPPORTED_PRODUCTS.keys())) + print( + f"Usage: python {sys.argv[0]} \n" + f"Supported products: {supported}" + ) + sys.exit(1) + + product = sys.argv[1].lower() + + config = get_product_config(product) + + full_version, cycle = get_version_info(config) + + eol_date = get_eol_date(config, cycle) + + days_remaining = calculate_days_remaining(eol_date) + + status = "OK" + + if days_remaining < THRESHOLD_DAYS: + status = "WARNING" + + prefix = product.upper() + + # ---- structured output for GitHub Actions ---- + print(f"STATUS={status}") + print(f"{prefix}_VERSION={full_version}") + print(f"{prefix}_CYCLE={cycle}") + print(f"EOL_DATE={eol_date}") + print(f"DAYS_REMAINING={days_remaining}") + + # ---- human-readable log ---- + print(f"\n--- {config['display_name']} EOL Report ---") + print(f"Version: {full_version}") + print(f"Cycle: {cycle}") + print(f"EOL date: {eol_date}") + print(f"Days remaining: {days_remaining}") + print(f"Status: {status}") + + # keep non-blocking behavior + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file From 9d326810528ca54ce745f08d2130afd90b4426df Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 10:23:55 +0200 Subject: [PATCH 044/126] Fix: fix python version --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cdf5f47a044..dad68896979 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -339,7 +339,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: Install system dependencies if: matrix.product == 'django' From 9eb047cd19f2d2b7e4ebfdd82efc918c7b62f562 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 10:27:29 +0200 Subject: [PATCH 045/126] Feat: Combine github comments --- .github/workflows/test.yml | 150 ++++++++++++++++++++++--------------- 1 file changed, 91 insertions(+), 59 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dad68896979..7ac54b83d2b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -360,68 +360,100 @@ jobs: fi - name: Run EOL check - id: eol run: | - output=$(python scripts/check_eol.py "${{ matrix.product }}" || true) + mkdir -p eol-results - echo "$output" + python scripts/check_eol.py "${{ matrix.product }}" \ + > "eol-results/${{ matrix.product }}.txt" || true - { - echo "output<> "$GITHUB_OUTPUT" + cat "eol-results/${{ matrix.product }}.txt" - - name: Parse EOL status - id: parse - run: | - STATUS=$(echo "${{ steps.eol.outputs.output }}" | grep STATUS= | cut -d= -f2) - echo "status=$STATUS" >> "$GITHUB_OUTPUT" + - name: Upload EOL result + uses: actions/upload-artifact@v4 + with: + name: eol-${{ matrix.product }} + path: eol-results/${{ matrix.product }}.txt - - name: Comment on PR if EOL warning - if: github.event_name == 'pull_request' && steps.parse.outputs.status == 'WARNING' - uses: actions/github-script@v7 - env: - EOL_OUTPUT: ${{ steps.eol.outputs.output }} - PRODUCT: ${{ matrix.product }} + eol-comment: + name: Consolidated EOL comment + runs-on: ubuntu-latest + needs: eol-check + + if: github.event_name == 'pull_request' + + permissions: + pull-requests: write + contents: read + + steps: + - name: Download EOL artifacts + uses: actions/download-artifact@v4 with: - script: | - const output = process.env.EOL_OUTPUT || ''; - const product = process.env.PRODUCT || 'dependency'; - - const marker = ``; - - const body = - marker + '\n' + - '```text\n' + - output.replace(/```/g, '``\\`') + - '\n```\n\n' + - `This ${product} version is approaching or past End-of-Life.\n` + - 'Please plan an upgrade.'; - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existingComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes(marker) - ); - - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body - }); - } + path: artifacts + + - name: Combine warnings + id: combine + shell: bash + run: | + combined="" + + while IFS= read -r -d '' file; do + if grep -q '^STATUS=WARNING' "$file"; then + combined="${combined} + $(cat "$file") + " + fi + done < <(find artifacts -name '*.txt' -print0) + + { + echo "warnings<> "$GITHUB_OUTPUT" + + - name: Create or update PR comment + if: steps.combine.outputs.warnings != '' + uses: actions/github-script@v7 + env: + WARNINGS: ${{ steps.combine.outputs.warnings }} + with: + script: | + const warnings = process.env.WARNINGS || ''; + + const marker = ''; + + const body = + marker + '\n' + + '# Dependency EOL Warnings\n\n' + + '```text\n' + + warnings.replace(/```/g, '``\\`') + + '\n```\n\n' + + 'One or more dependencies are approaching or past End-of-Life.\n' + + 'Please plan upgrades accordingly.'; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes(marker) + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } From 256c7cc81d6d90c68c8bcb6ae8de585b7735f2da Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 10:33:48 +0200 Subject: [PATCH 046/126] Fix: Indentation --- .github/workflows/test.yml | 92 +++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ac54b83d2b..50c4d8e6361 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -411,49 +411,49 @@ jobs: echo "EOF" } >> "$GITHUB_OUTPUT" - - name: Create or update PR comment - if: steps.combine.outputs.warnings != '' - uses: actions/github-script@v7 - env: - WARNINGS: ${{ steps.combine.outputs.warnings }} - with: - script: | - const warnings = process.env.WARNINGS || ''; - - const marker = ''; - - const body = - marker + '\n' + - '# Dependency EOL Warnings\n\n' + - '```text\n' + - warnings.replace(/```/g, '``\\`') + - '\n```\n\n' + - 'One or more dependencies are approaching or past End-of-Life.\n' + - 'Please plan upgrades accordingly.'; - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existingComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes(marker) - ); - - if (existingComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existingComment.id, - body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body - }); - } + - name: Create or update PR comment + if: steps.combine.outputs.warnings != '' + uses: actions/github-script@v7 + env: + WARNINGS: ${{ steps.combine.outputs.warnings }} + with: + script: | + const warnings = process.env.WARNINGS || ''; + + const marker = ''; + + const body = + marker + '\n' + + '# Dependency EOL Warnings\n\n' + + '```text\n' + + warnings.replace(/```/g, '``\\`') + + '\n```\n\n' + + 'One or more dependencies are approaching or past End-of-Life.\n' + + 'Please plan upgrades accordingly.'; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes(marker) + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } From f89833d1bee9b9d3b716a321b380881ba5847369 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 11:12:57 +0200 Subject: [PATCH 047/126] Refactor: Allow expension of EOL tracking --- .github/workflows/test.yml | 41 +++------- scripts/check_eol.py | 152 ------------------------------------- scripts/eol/check_eol.py | 10 +++ scripts/eol/eol_api.py | 26 +++++++ scripts/eol/runner.py | 40 ++++++++++ scripts/eol/runtimes.py | 24 ++++++ scripts/eol/scanner.py | 74 ++++++++++++++++++ 7 files changed, 186 insertions(+), 181 deletions(-) delete mode 100644 scripts/check_eol.py create mode 100644 scripts/eol/check_eol.py create mode 100644 scripts/eol/eol_api.py create mode 100644 scripts/eol/runner.py create mode 100644 scripts/eol/runtimes.py create mode 100644 scripts/eol/scanner.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50c4d8e6361..3648667ff5a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -323,12 +323,6 @@ jobs: continue-on-error: true timeout-minutes: 10 - strategy: - matrix: - product: - - django - - python - permissions: pull-requests: write contents: read @@ -341,38 +335,27 @@ jobs: with: python-version: "3.12" - - name: Install system dependencies - if: matrix.product == 'django' - run: | - sudo apt-get update - sudo apt-get install -y \ - libldap2-dev \ - libsasl2-dev \ - libssl-dev - - - name: Install Python dependencies + - name: Install dependencies run: | python -m pip install --upgrade pip pip install requests - if [ "${{ matrix.product }}" = "django" ]; then - pip install -r requirements.txt - fi + - name: Install Django deps + run: | + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Run EOL check + id: eol run: | - mkdir -p eol-results - - python scripts/check_eol.py "${{ matrix.product }}" \ - > "eol-results/${{ matrix.product }}.txt" || true + output=$(python scripts/check_eol.py || true) - cat "eol-results/${{ matrix.product }}.txt" + echo "$output" - - name: Upload EOL result - uses: actions/upload-artifact@v4 - with: - name: eol-${{ matrix.product }} - path: eol-results/${{ matrix.product }}.txt + { + echo "output<> "$GITHUB_OUTPUT" eol-comment: name: Consolidated EOL comment diff --git a/scripts/check_eol.py b/scripts/check_eol.py deleted file mode 100644 index d7ccb5b914b..00000000000 --- a/scripts/check_eol.py +++ /dev/null @@ -1,152 +0,0 @@ -import requests -import datetime -import sys -import subprocess -import re -from requests import RequestException - -THRESHOLD_DAYS = 180 - -SUPPORTED_PRODUCTS = { - "django": { - "api": "https://endoflife.date/api/django.json", - "version_command": [sys.executable, "-m", "django", "--version"], - "version_pattern": r"^(\d+)\.(\d+)", - "display_name": "Django", - }, - "python": { - "api": "https://endoflife.date/api/python.json", - "version_command": [sys.executable, "--version"], - "version_pattern": r"^Python (\d+)\.(\d+)", - "display_name": "Python", - }, -} - - -def get_product_config(product): - config = SUPPORTED_PRODUCTS.get(product) - - if not config: - supported = ", ".join(sorted(SUPPORTED_PRODUCTS.keys())) - print( - f"ERROR: Unsupported product '{product}'. " - f"Supported products: {supported}" - ) - sys.exit(1) - - return config - - -def get_version_info(config): - try: - result = subprocess.run( - config["version_command"], - capture_output=True, - text=True, - timeout=30 - ) - except subprocess.TimeoutExpired: - print("ERROR: Version command timed out") - sys.exit(1) - - if result.returncode != 0: - print("ERROR: Unable to determine version") - sys.exit(1) - - version_output = result.stdout.strip() - - m = re.match(config["version_pattern"], version_output) - - if not m: - print(f"ERROR: Unexpected version format: {version_output}") - sys.exit(1) - - cycle = f"{m.group(1)}.{m.group(2)}" - - if config["display_name"] == "Python": - full_version = version_output.replace("Python ", "") - else: - full_version = version_output - - return full_version, cycle - - -def get_eol_date(config, cycle): - try: - response = requests.get(config["api"], timeout=10) - response.raise_for_status() - except RequestException as exc: - print(f"ERROR: Failed to fetch EOL data: {exc}") - sys.exit(1) - - data = response.json() - - for release in data: - if release["cycle"] == cycle: - return release["eol"] - - print( - f"ERROR: No EOL data found for " - f"{config['display_name']} {cycle}" - ) - sys.exit(1) - - -def calculate_days_remaining(eol_date): - today = datetime.date.today() - - eol = datetime.datetime.strptime( - eol_date, - "%Y-%m-%d" - ).date() - - return (eol - today).days - - -def main(): - if len(sys.argv) != 2: - supported = ", ".join(sorted(SUPPORTED_PRODUCTS.keys())) - print( - f"Usage: python {sys.argv[0]} \n" - f"Supported products: {supported}" - ) - sys.exit(1) - - product = sys.argv[1].lower() - - config = get_product_config(product) - - full_version, cycle = get_version_info(config) - - eol_date = get_eol_date(config, cycle) - - days_remaining = calculate_days_remaining(eol_date) - - status = "OK" - - if days_remaining < THRESHOLD_DAYS: - status = "WARNING" - - prefix = product.upper() - - # ---- structured output for GitHub Actions ---- - print(f"STATUS={status}") - print(f"{prefix}_VERSION={full_version}") - print(f"{prefix}_CYCLE={cycle}") - print(f"EOL_DATE={eol_date}") - print(f"DAYS_REMAINING={days_remaining}") - - # ---- human-readable log ---- - print(f"\n--- {config['display_name']} EOL Report ---") - print(f"Version: {full_version}") - print(f"Cycle: {cycle}") - print(f"EOL date: {eol_date}") - print(f"Days remaining: {days_remaining}") - print(f"Status: {status}") - - # keep non-blocking behavior - sys.exit(0) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/eol/check_eol.py b/scripts/eol/check_eol.py new file mode 100644 index 00000000000..d5af06410a4 --- /dev/null +++ b/scripts/eol/check_eol.py @@ -0,0 +1,10 @@ +import sys +from eol.runner import run_all + + +if __name__ == "__main__": + if len(sys.argv) == 1: + run_all() + else: + from eol.runner import run_one + run_one(sys.argv[1].lower()) \ No newline at end of file diff --git a/scripts/eol/eol_api.py b/scripts/eol/eol_api.py new file mode 100644 index 00000000000..e070d695900 --- /dev/null +++ b/scripts/eol/eol_api.py @@ -0,0 +1,26 @@ +import requests +import datetime +import sys +from requests import RequestException + + +def get_eol_date(api_url: str, cycle: str): + try: + response = requests.get(api_url, timeout=10) + response.raise_for_status() + except RequestException as exc: + print(f"ERROR: Failed to fetch EOL data: {exc}") + sys.exit(1) + + for release in response.json(): + if release["cycle"] == cycle: + return release["eol"] + + print(f"ERROR: No EOL data found for cycle {cycle}") + sys.exit(1) + + +def calculate_days_remaining(eol_date: str) -> int: + today = datetime.date.today() + eol = datetime.datetime.strptime(eol_date, "%Y-%m-%d").date() + return (eol - today).days \ No newline at end of file diff --git a/scripts/eol/runner.py b/scripts/eol/runner.py new file mode 100644 index 00000000000..865b788d920 --- /dev/null +++ b/scripts/eol/runner.py @@ -0,0 +1,40 @@ +from eol.scanner import scan_repo +from eol.runtimes import RUNTIMES +from eol.eol_api import get_eol_date, calculate_days_remaining + +THRESHOLD_DAYS = 180 + + +def run_one(product): + run_all(filter_product=product) + + +def run_all(filter_product=None): + detected = scan_repo() + + if filter_product: + detected = {filter_product: detected.get(filter_product)} + + for name, version in detected.items(): + if not version: + continue + + runtime = RUNTIMES.get(name) + if not runtime: + continue + + eol_date = get_eol_date(runtime.api, version) + days = calculate_days_remaining(eol_date) + + status = "WARNING" if days < THRESHOLD_DAYS else "OK" + + print(f"STATUS={status}") + print(f"{name.upper()}_VERSION={version}") + print(f"{name.upper()}_CYCLE={version}") + print(f"EOL_DATE={eol_date}") + print(f"DAYS_REMAINING={days}") + + print(f"\n--- {runtime.display_name} ---") + print(f"Version: {version}") + print(f"EOL: {eol_date}") + print(f"Status: {status}\n") \ No newline at end of file diff --git a/scripts/eol/runtimes.py b/scripts/eol/runtimes.py new file mode 100644 index 00000000000..6918c4d9e89 --- /dev/null +++ b/scripts/eol/runtimes.py @@ -0,0 +1,24 @@ +class Runtime: + def __init__(self, name, api, display_name): + self.name = name + self.api = api + self.display_name = display_name + + +RUNTIMES = { + "python": Runtime( + "python", + "https://endoflife.date/api/python.json", + "Python", + ), + "node": Runtime( + "node", + "https://endoflife.date/api/nodejs.json", + "Node.js", + ), + "django": Runtime( + "django", + "https://endoflife.date/api/django.json", + "Django", + ), +} \ No newline at end of file diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py new file mode 100644 index 00000000000..35044e771be --- /dev/null +++ b/scripts/eol/scanner.py @@ -0,0 +1,74 @@ +import json +import re +from pathlib import Path + + +def detect_node(): + pkg = Path("package.json") + + if not pkg.exists(): + return None + + try: + data = json.loads(pkg.read_text()) + engines = data.get("engines", {}) + node = engines.get("node") + + if not node: + return None + + m = re.search(r"(\d+)", node) + return f"{m.group(1)}.0" if m else None + except Exception: + return None + + +def detect_python(): + if Path("pyproject.toml").exists() or Path("requirements.txt").exists(): + return "3.12" + return None + + +def detect_django(): + req_file = Path("requirements.txt") + + if not req_file.exists(): + return None + + content = req_file.read_text() + + match = re.search( + r"(?i)^django\s*([<>=!~]=?)\s*([\d\.]+)", + content, + re.MULTILINE + ) + + if not match: + return None + + version = match.group(2) + + # Normalize to major.minor + parts = version.split(".") + if len(parts) >= 2: + return f"{parts[0]}.{parts[1]}" + + return f"{parts[0]}.0" + + +def scan_repo(): + results = {} + + node = detect_node() + if node: + results["node"] = node + + python = detect_python() + if python: + results["python"] = python + + django = detect_django() + if django: + results["django"] = django + + return results \ No newline at end of file From 6b7457a1b9e115825ade6bad202cf581dcb66900 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 11:46:03 +0200 Subject: [PATCH 048/126] Fix: remove install --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3648667ff5a..c54813837d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -340,10 +340,6 @@ jobs: python -m pip install --upgrade pip pip install requests - - name: Install Django deps - run: | - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Run EOL check id: eol run: | From 20f804e3b63b67243e61dad176f6f8936663a58c Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 11:58:34 +0200 Subject: [PATCH 049/126] Fix: fix path --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c54813837d2..d5425fecf41 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -343,7 +343,7 @@ jobs: - name: Run EOL check id: eol run: | - output=$(python scripts/check_eol.py || true) + output=$(python scripts/eol/check_eol.py || true) echo "$output" From 459d299ddd62242fa2914d5e2edaefb6d1ef1fb2 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 12:15:59 +0200 Subject: [PATCH 050/126] Fix: Update imports --- scripts/eol/check_eol.py | 3 ++- scripts/eol/runner.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/eol/check_eol.py b/scripts/eol/check_eol.py index d5af06410a4..e43118cf73c 100644 --- a/scripts/eol/check_eol.py +++ b/scripts/eol/check_eol.py @@ -1,5 +1,6 @@ import sys -from eol.runner import run_all + +from scripts.eol.runner import run_all if __name__ == "__main__": diff --git a/scripts/eol/runner.py b/scripts/eol/runner.py index 865b788d920..b646b1514db 100644 --- a/scripts/eol/runner.py +++ b/scripts/eol/runner.py @@ -1,6 +1,6 @@ -from eol.scanner import scan_repo -from eol.runtimes import RUNTIMES -from eol.eol_api import get_eol_date, calculate_days_remaining +from scripts.eol.scanner import scan_repo +from scripts.eol.runtimes import RUNTIMES +from scripts.eol.eol_api import get_eol_date, calculate_days_remaining THRESHOLD_DAYS = 180 From 4890ad7c1ff69933daa81b2d8f5c40d39d6a46d9 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Tue, 12 May 2026 12:24:45 +0200 Subject: [PATCH 051/126] Fix: Add init --- .github/workflows/test.yml | 2 +- scripts/__init__.py | 0 scripts/eol/__init__.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 scripts/__init__.py create mode 100644 scripts/eol/__init__.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d5425fecf41..013a525ebf3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -343,7 +343,7 @@ jobs: - name: Run EOL check id: eol run: | - output=$(python scripts/eol/check_eol.py || true) + output=$(python -m scripts.eol.check_eol || true) echo "$output" diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scripts/eol/__init__.py b/scripts/eol/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 9260ff5b5069a55803cd11fa28df527347e319bd Mon Sep 17 00:00:00 2001 From: "Caroline D." <108160931+CarolineDenis@users.noreply.github.com> Date: Wed, 13 May 2026 12:40:50 +0200 Subject: [PATCH 052/126] Update .github/workflows/test.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 013a525ebf3..fedd18a07d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -346,12 +346,20 @@ jobs: output=$(python -m scripts.eol.check_eol || true) echo "$output" + + echo "$output" > eol-output.txt { echo "output<> "$GITHUB_OUTPUT" + + - name: Upload EOL results + uses: actions/upload-artifact@v4 + with: + name: eol-results + path: eol-output.txt eol-comment: name: Consolidated EOL comment From 2da603a7d2b831f8092921ae19a2dbd6aede0114 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 10:46:45 +0000 Subject: [PATCH 053/126] fix: apply CodeRabbit auto-fixes Fixed 3 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit --- scripts/eol/check_eol.py | 2 +- scripts/eol/eol_api.py | 14 +++++-- scripts/eol/scanner.py | 90 ++++++++++++++++++++++++++++++++-------- 3 files changed, 83 insertions(+), 23 deletions(-) diff --git a/scripts/eol/check_eol.py b/scripts/eol/check_eol.py index e43118cf73c..dd4cf5213af 100644 --- a/scripts/eol/check_eol.py +++ b/scripts/eol/check_eol.py @@ -7,5 +7,5 @@ if len(sys.argv) == 1: run_all() else: - from eol.runner import run_one + from scripts.eol.runner import run_one run_one(sys.argv[1].lower()) \ No newline at end of file diff --git a/scripts/eol/eol_api.py b/scripts/eol/eol_api.py index e070d695900..bb6611266de 100644 --- a/scripts/eol/eol_api.py +++ b/scripts/eol/eol_api.py @@ -20,7 +20,13 @@ def get_eol_date(api_url: str, cycle: str): sys.exit(1) -def calculate_days_remaining(eol_date: str) -> int: - today = datetime.date.today() - eol = datetime.datetime.strptime(eol_date, "%Y-%m-%d").date() - return (eol - today).days \ No newline at end of file +def calculate_days_remaining(eol_date: str): + if not isinstance(eol_date, str) or not eol_date or eol_date is False: + return None + + try: + today = datetime.date.today() + eol = datetime.datetime.strptime(eol_date, "%Y-%m-%d").date() + return (eol - today).days + except ValueError: + return None \ No newline at end of file diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index 35044e771be..3bc5bf487f5 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -4,28 +4,82 @@ def detect_node(): - pkg = Path("package.json") + # Search for package.json files recursively + for pkg in Path(".").rglob("package.json"): + try: + data = json.loads(pkg.read_text()) + engines = data.get("engines", {}) + node = engines.get("node") + + if not node: + continue + + m = re.search(r"(\d+)", node) + if m: + return f"{m.group(1)}.0" + except (json.JSONDecodeError, OSError, ValueError): + continue - if not pkg.exists(): - return None - - try: - data = json.loads(pkg.read_text()) - engines = data.get("engines", {}) - node = engines.get("node") - - if not node: - return None - - m = re.search(r"(\d+)", node) - return f"{m.group(1)}.0" if m else None - except Exception: - return None + return None def detect_python(): - if Path("pyproject.toml").exists() or Path("requirements.txt").exists(): - return "3.12" + # Try pyproject.toml + pyproject = Path("pyproject.toml") + if pyproject.exists(): + try: + content = pyproject.read_text() + # Look for requires-python in pyproject.toml + match = re.search(r'requires-python\s*=\s*["\']([^"\']+)["\']', content) + if match: + version_spec = match.group(1) + # Extract X.Y format from version specs like ">=3.12" or "^3.12" + version_match = re.search(r"(\d+)\.(\d+)", version_spec) + if version_match: + return f"{version_match.group(1)}.{version_match.group(2)}" + except (OSError, ValueError): + pass + + # Try .python-version + python_version = Path(".python-version") + if python_version.exists(): + try: + content = python_version.read_text().strip() + # Extract X.Y format + match = re.search(r"^(\d+)\.(\d+)", content) + if match: + return f"{match.group(1)}.{match.group(2)}" + except OSError: + pass + + # Try runtime.txt + runtime = Path("runtime.txt") + if runtime.exists(): + try: + content = runtime.read_text().strip() + # Look for python-X.Y pattern + match = re.search(r"python-(\d+)\.(\d+)", content) + if match: + return f"{match.group(1)}.{match.group(2)}" + except OSError: + pass + + # Try GitHub Actions workflows + workflows_dir = Path(".github/workflows") + if workflows_dir.exists(): + try: + for workflow in workflows_dir.glob("*.yml"): + try: + content = workflow.read_text() + # Look for setup-python with python-version + match = re.search(r"python-version:\s*[\"']?(\d+\.\d+)", content) + if match: + return match.group(1) + except OSError: + continue + except OSError: + pass + return None From c490002d5fa3bf9be609a21ab03c88cfd79426d1 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 13 May 2026 13:10:43 +0200 Subject: [PATCH 054/126] Fix: Handle None dates --- scripts/eol/runner.py | 5 ++++- scripts/eol/scanner.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/eol/runner.py b/scripts/eol/runner.py index b646b1514db..3446880ac17 100644 --- a/scripts/eol/runner.py +++ b/scripts/eol/runner.py @@ -26,7 +26,10 @@ def run_all(filter_product=None): eol_date = get_eol_date(runtime.api, version) days = calculate_days_remaining(eol_date) - status = "WARNING" if days < THRESHOLD_DAYS else "OK" + if days is None: + status = "UNKNOWN" + else: + status = "WARNING" if days < THRESHOLD_DAYS else "OK" print(f"STATUS={status}") print(f"{name.upper()}_VERSION={version}") diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index 3bc5bf487f5..96202bbe012 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -16,7 +16,7 @@ def detect_node(): m = re.search(r"(\d+)", node) if m: - return f"{m.group(1)}.0" + return m.group(1) except (json.JSONDecodeError, OSError, ValueError): continue From ea2f299b3e78644294f28440a8e476aa90d61252 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 13 May 2026 13:32:52 +0200 Subject: [PATCH 055/126] Fix: Handle string and integer cycle --- scripts/eol/eol_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/eol/eol_api.py b/scripts/eol/eol_api.py index bb6611266de..18146317aed 100644 --- a/scripts/eol/eol_api.py +++ b/scripts/eol/eol_api.py @@ -13,7 +13,7 @@ def get_eol_date(api_url: str, cycle: str): sys.exit(1) for release in response.json(): - if release["cycle"] == cycle: + if str(release["cycle"]) == cycle: return release["eol"] print(f"ERROR: No EOL data found for cycle {cycle}") From 434314270c306776d461fcb0ceff1393815bda57 Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 13 May 2026 13:34:10 +0200 Subject: [PATCH 056/126] Fix: handle failed request --- scripts/eol/scanner.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index 96202bbe012..bad87718886 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -89,7 +89,10 @@ def detect_django(): if not req_file.exists(): return None - content = req_file.read_text() + try: + content = req_file.read_text() + except OSError: + return None match = re.search( r"(?i)^django\s*([<>=!~]=?)\s*([\d\.]+)", From 692d5fc09352ed5aa2e9d76ce45c58b1cabddadc Mon Sep 17 00:00:00 2001 From: CarolineDenis Date: Wed, 13 May 2026 13:34:54 +0200 Subject: [PATCH 057/126] Fix: Improve regex --- scripts/eol/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index bad87718886..47c5f163c93 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -95,7 +95,7 @@ def detect_django(): return None match = re.search( - r"(?i)^django\s*([<>=!~]=?)\s*([\d\.]+)", + r"(?i)^django\b\s*([<>=!~]=?)\s*([\d\.]+)", content, re.MULTILINE ) From 928846c101b7a5310a2dfd634d519fd00cf7e6b4 Mon Sep 17 00:00:00 2001 From: "Caroline D." <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:23:16 +0200 Subject: [PATCH 058/126] Potential fix for pull request finding 'CodeQL / Empty except' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- scripts/eol/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index 47c5f163c93..2022f0fbb2f 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -38,7 +38,7 @@ def detect_python(): if version_match: return f"{version_match.group(1)}.{version_match.group(2)}" except (OSError, ValueError): - pass + return None # Try .python-version python_version = Path(".python-version") From fdd7a93177b34665405a93e0d5c6d6f734c8d438 Mon Sep 17 00:00:00 2001 From: "Caroline D." <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:24:03 +0200 Subject: [PATCH 059/126] Potential fix for pull request finding 'CodeQL / Empty except' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- scripts/eol/scanner.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index 2022f0fbb2f..0e3b2161856 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -45,12 +45,12 @@ def detect_python(): if python_version.exists(): try: content = python_version.read_text().strip() - # Extract X.Y format - match = re.search(r"^(\d+)\.(\d+)", content) - if match: - return f"{match.group(1)}.{match.group(2)}" except OSError: - pass + content = "" + # Extract X.Y format + match = re.search(r"^(\d+)\.(\d+)", content) + if match: + return f"{match.group(1)}.{match.group(2)}" # Try runtime.txt runtime = Path("runtime.txt") From 406a949933ceadc83ce2a6b907cec43017d1efa3 Mon Sep 17 00:00:00 2001 From: "Caroline D." <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:24:20 +0200 Subject: [PATCH 060/126] Potential fix for pull request finding 'CodeQL / Empty except' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- scripts/eol/scanner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index 0e3b2161856..274936ce93c 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -62,7 +62,8 @@ def detect_python(): if match: return f"{match.group(1)}.{match.group(2)}" except OSError: - pass + # Ignore unreadable runtime.txt and continue with other detection strategies. + continue # Try GitHub Actions workflows workflows_dir = Path(".github/workflows") From 11416508f8d60a224044cc0fee6c6851ccb029cd Mon Sep 17 00:00:00 2001 From: "Caroline D." <108160931+CarolineDenis@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:24:28 +0200 Subject: [PATCH 061/126] Potential fix for pull request finding 'CodeQL / Empty except' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- scripts/eol/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index 274936ce93c..31e037f173d 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -79,7 +79,7 @@ def detect_python(): except OSError: continue except OSError: - pass + continue return None From f6a9051301feeb942e7b5ab95e78d317401dd105 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:32:42 -0500 Subject: [PATCH 062/126] fix: use pass instead of continue in scanner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids using 'continue' where it may be invalid– it stopped me from running it properly without this change --- scripts/eol/scanner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/eol/scanner.py b/scripts/eol/scanner.py index 31e037f173d..db30b93c52c 100644 --- a/scripts/eol/scanner.py +++ b/scripts/eol/scanner.py @@ -63,7 +63,7 @@ def detect_python(): return f"{match.group(1)}.{match.group(2)}" except OSError: # Ignore unreadable runtime.txt and continue with other detection strategies. - continue + pass # Try GitHub Actions workflows workflows_dir = Path(".github/workflows") @@ -79,7 +79,7 @@ def detect_python(): except OSError: continue except OSError: - continue + pass return None From 6e23b6117b79b5c71d2cb6120878062eb21aa359 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:41:03 -0500 Subject: [PATCH 063/126] fix: update about info --- .../js_src/lib/localization/welcome.ts | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/welcome.ts b/specifyweb/frontend/js_src/lib/localization/welcome.ts index d73d94af520..ff6a77212cc 100644 --- a/specifyweb/frontend/js_src/lib/localization/welcome.ts +++ b/specifyweb/frontend/js_src/lib/localization/welcome.ts @@ -93,41 +93,11 @@ export const welcomeText = createDictionary({ }, disclosure: { 'en-us': - "Specify software is a product of the Specify Collections Consortium that is governed and funded by its member institutions. Consortium Founding Members include: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Denmark Consortium of Museums, Muséum d'Histoire Naturelle Geneva, University of Florida, University of Kansas, and University of Michigan. The Consortium operates under the non-profit, 501(c)3, U.S. tax status of the University of Kansas Center for Research. Specify was supported from 1996 to 2018 by grants from the U.S. National Science Foundation.", - 'ru-ru': - "Specify software является продуктом консорциума Specify Collections. который управляется и финансируется организациями-членами. Члены-учредители консорциума включают: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Denmark Consortium of Museums, Muséum d'Histoire Naturelle Geneva, University of Florida, University of Kansas, и University of Michigan. Консорциум действует под некоммерческой организацией, 501(c)3, налоговым статусом США университета University of Kansas. Specify поддерживался с 1996 по 2018 год грантами фонда U.S. National Science Foundation.", - 'es-es': - "Specify Software es un producto de Specify Collections Consortium, financiado por sus instituciones miembro. Los Miembros Fundadores del Consorcio incluyen: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas (CSIC), Denmark Consortium of Museums, Muséum d'Histoire Naturelle Geneva, University of Florida, University of Kansas y University of Michigan. El Consorcio opera bajo las condiciones fiscales de 501(c)3 de EE.UU. como organización sin ánimo de lucro, University of Kansas Center for Research. Specify ha sido financiado entre 1996 y 2018 por múltiples ayudas de U.S. National Science Foundation.", - 'fr-fr': - "Le logiciel Specify est un produit du Specify Collections Consortium qui est régi et financé par ses institutions membres. Les membres fondateurs du consortium comprennent : l'Organisation de recherche scientifique et industrielle du Commonwealth (CSIRO), le Consejo Superior de Investigaciones Científicas, le Consortium danois des musées, le Muséum d'Histoire Naturelle de Genève, l'Université de Floride, l'Université du Kansas et l'Université du Michigan. Le Consortium opère sous le statut fiscal américain à but non lucratif 501(c)3 du Centre de recherche de l'Université du Kansas. Specify a été soutenu de 1996 à 2018 par des subventions de la National Science Foundation des États-Unis.", - 'uk-ua': - "Програмне забезпечення Specify є продуктом консорціуму Specify Collections Consortium, яким керують і фінансують установи-члени. Члени-засновники консорціуму включають: Науково-промислову дослідницьку організацію Співдружності (CSIRO), Consejo Superior de Investigaciones Sientíficas, Датський консорціум музеїв, Muséum d'Histoire Naturelle Geneva, Університет Флориди, Університет Канзасу та Університет Мічигану. Консорціум працює відповідно до некомерційного, 501(c)3, податкового статусу США дослідницького центру Канзаського університету. З 1996 по 2018 рік Specify підтримувався грантами Національного наукового фонду США.", - 'de-ch': - "Die Specify-Software ist ein Produkt des Specify Collections Consortiums, das von seinen Mitgliedsinstitutionen verwaltet und finanziert wird. Zu den Gründungsmitgliedern des Konsortiums gehören: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Denmark Consortium of Museums, Muséum d'Histoire Naturelle Geneva, University of Florida, University of Kansas, University of Michigan. Das Konsortium arbeitet unter dem gemeinnützigen, 501(c)3, U.S. Steuerstatus des University of Kansas Center for Research. Specify wurde von 1996 bis 2018 durch Zuschüsse der U.S. National Science Foundation unterstützt.", - 'pt-br': - "O software Specify é um produto do Specify Collections Consortium, que é governado e financiado por suas instituições membros. Os membros fundadores do Consórcio incluem: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Denmark Consortium of Museums, Muséum d'Histoire Naturelle Geneva, University of Florida, University of Kansas e University of Michigan. O Consórcio opera sob o status de organização sem fins lucrativos, 501(c)3, do Centro de Pesquisa da Universidade do Kansas, nos EUA. O Specify foi financiado de 1996 a 2018 por bolsas da Fundação Nacional de Ciência dos EUA (NSF).", - 'hr-hr': - "Specify softver je proizvod konzorcija Specify Collections kojim upravljaju i financiraju ga njegove institucije članice. Osnivači konzorcija uključuju: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Danski konzorcij muzeja, Muséum d'Histoire Naturelle Geneva, Sveučilište Florida, Sveučilište Kansas i Sveučilište Michigan. Konzorcij djeluje pod neprofitnim, poreznim statusom 501(c)3 Sveučilišnog centra za istraživanje u Kansasu. Specify je od 1996. do 2018. godine financiran bespovratnim sredstvima američke Nacionalne zaklade za znanost.", - nb: "Specify-programvaren er et produkt fra Specify Collections Consortium, som styres og finansieres av medlemsinstitusjonene. Blant konsortiets grunnleggere er: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Denmark Consortium of Museums, Muséum d'Histoire Naturelle Geneva, University of Florida, University of Kansas og University of Michigan. Konsortiet opererer under den ideelle skatteformen 501(c)(3) i USA, gjennom University of Kansas Center for Research. Specify ble støttet fra 1996 til 2018 av tilskudd fra U.S. National Science Foundation.", + "Specify is developed by the Specify Collections Consortium (SCC), a collaborative initiative governed by its members and supported by institutional partners. Software support and development are made possible by consortium members, including the UniMus:Natur Consortium of Norway, the Commonwealth Scientific and Industrial Research Organisation (CSIRO), the Consejo Superior de Investigaciones Científicas (CSIC), the Denmark Consortium of Museums, the Muséum d’Histoire Naturelle Geneva, the University of Florida, the University of Kansas, and the University of Michigan, along with numerous other member collections and institutions within the Consortium. The SCC operates under the University of Kansas Center for Research’s non-profit 501(c)(3) U.S. tax status and received support from U.S. National Science Foundation grants from 1996 to 2018.", }, licence: { 'en-us': - 'Specify 7, Copyright 2025, University of Kansas Center for Research. Specify comes with ABSOLUTELY NO WARRANTY. This is free, open-source software licensed under GNU General Public License v2.', - 'ru-ru': - 'Specify 7, Авторские права 2025, University of Kansas для исследования. Specify поставляется с СОВЕРШЕННО ОТСУТСТВИЕМ ГАРАНТИИ. Это бесплатное программное обеспечение с открытым исходным кодом под лицензией GNU General Public License v2.', - 'es-es': - 'Specify 7 Copyright © 2025 University of Kansas Center for Research. Specify viene SIN NINGUNA GARANTÍA EN ABSOLUTO. Este es un programa libre, bajo licencia GNU General Public License 2 (GPL2).', - 'fr-fr': - "Specify 7, © 2025, Centre de recherche de l'Université du Kansas. Specify est fourni SANS AUCUNE GARANTIE. Ce logiciel libre et open source est distribué sous licence GNU GPL v2.", - 'uk-ua': - 'Укажіть 7, авторське право 2025, Дослідницький центр Канзаського університету. Specify поставляється без АБСОЛЮТНОЇ ГАРАНТІЇ. Це безкоштовне програмне забезпечення з відкритим кодом, ліцензоване згідно з GNU General Public License v2.', - 'de-ch': - 'Specify 7, Copyright 2025, University of Kansas Center for Research. Specify kommt mit ABSOLUT KEINER GARANTIE. Dies ist freie, quelloffene Software, lizenziert unter GNU General Public License v2.', - 'pt-br': - 'Specify 7, Copyright 2025, Centro de Pesquisa da Universidade do Kansas. Specify é fornecido SEM QUALQUER GARANTIA. Este é um software livre e de código aberto licenciado sob a Licença Pública Geral GNU v2.', - 'hr-hr': - 'Specify 7, autorsko pravo 2025., Sveučilišni centar za istraživanje u Kansasu. Specify dolazi APSOLUTNO BEZ JAMSTVA. Ovo je besplatni softver otvorenog koda licenciran pod GNU General Public License v2.', - nb: 'Specify 7, Copyright 2025, University of Kansas Center for Research. Specify leveres UTEN NOEN FORM FOR GARANTI. Dette er fri, åpen kildekode-programvare, lisensiert under GNU General Public License v2.', + 'Specify 7, Copyright 2026, University of Kansas Center for Research. Specify comes with ABSOLUTELY NO WARRANTY. This is free, open-source software licensed under GNU General Public License v3.', }, systemInformation: { 'en-us': 'System Information', From 093821608d005465a5a9d72848fb9137e99d9889 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:49:46 -0500 Subject: [PATCH 064/126] chore: minor style adjustment --- .github/workflows/test.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fedd18a07d2..82e9d6320a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -411,12 +411,13 @@ jobs: const body = marker + '\n' + - '# Dependency EOL Warnings\n\n' + + '> [!WARNING]\n' + + '> One or more dependencies are approaching or past End-of-Life.\n' + + '> Please plan upgrades accordingly.\n' + + '\n' + '```text\n' + warnings.replace(/```/g, '``\\`') + - '\n```\n\n' + - 'One or more dependencies are approaching or past End-of-Life.\n' + - 'Please plan upgrades accordingly.'; + '\n```'; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, From aac284b4557ce6e1cd8e86b9b5a7beeecd84fb05 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 7 Jul 2026 16:00:00 -0500 Subject: [PATCH 065/126] created a permit object --- .../businessrules/tests/test_permit.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 770fe863725..83f362d07ac 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -2,6 +2,7 @@ from specifyweb.specify import models from specifyweb.specify.tests.test_api import ApiTests from ..exceptions import BusinessRuleException +import datetime class PermitTests(ApiTests): @@ -32,15 +33,23 @@ def test_delete_blocked_by_accessionauthorization(self): aa.delete() permit.delete() - # Create a Permit with required fields only - def test_create_basic_permit(self): - permit = models.Permit.objects.create( # creates new Permit record in the database + def test_create_permit_with_fields(self): + # Fill in Permit#, Type, and Dates fields + # Fill in remaining fields + permit = models.Permit.objects.create( #creating a permit object institution=self.institution, - permitnumber='P-BASIC-001', + permitnumber='P-FIELDS-001', + type='Collection', + startdate=datetime.datetime(2024, 1, 1), + enddate=datetime.datetime(2024, 12, 31), + issueddate=datetime.datetime(2024, 1, 15), + renewaldate=datetime.datetime(2025, 1, 15), + status='Active', + remarks='Annual collection permit', + permittext='Authorized for scientific collection', + yesno1=True, + text1='Filed under drawer 3', + number1=42.5, ) + - # Verify save worked - self.assertIsNotNone(permit.id) # making sure this is not none - self.assertEqual(permit.version, 0) # making sure the version starts at 0 - fetched = models.Permit.objects.get(id=permit.id) # refetching the record - self.assertEqual(fetched.permitnumber, 'P-BASIC-001') # comparing the fetched permitnumber to the actual permit number From 7f80d0384515cb03bfd96a59722ab6e7ed02b9aa Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 7 Jul 2026 16:00:31 -0500 Subject: [PATCH 066/126] [test]: saved + verify all fields persisted --- .../backend/businessrules/tests/test_permit.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 83f362d07ac..0272676379e 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -51,5 +51,18 @@ def test_create_permit_with_fields(self): text1='Filed under drawer 3', number1=42.5, ) + + # Save + verify all fields persisted + self.assertIsNotNone(permit.id) + fetched = models.Permit.objects.get(id=permit.id) # fetch the permit object + self.assertEqual(fetched.permitnumber, 'P-FIELDS-001') # check all the things in + self.assertEqual(fetched.type, 'Collection') + self.assertEqual(fetched.startdate, datetime.datetime(2024, 1, 1)) + self.assertEqual(fetched.enddate, datetime.datetime(2024, 12, 31)) + self.assertEqual(fetched.issueddate, datetime.datetime(2024, 1, 15)) + self.assertEqual(fetched.renewaldate, datetime.datetime(2025, 1, 15)) + self.assertEqual(fetched.status, 'Active') + self.assertEqual(fetched.remarks, 'Annual collection permit') + self.assertEqual(fetched.yesno1, True) From c5537745a622a133b04287fa5e688ef9bc7306a6 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 09:37:37 -0500 Subject: [PATCH 067/126] [test]: create mew issued by agent --- specifyweb/backend/businessrules/tests/test_permit.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 0272676379e..96700e21986 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -66,3 +66,12 @@ def test_create_permit_with_fields(self): self.assertEqual(fetched.yesno1, True) + def test_create_permit_with_agents(self): + # Create new Issued By agent + new_issuedby = models.Agent.objects.create( + agenttype=0, + firstname="Issued", + lastname="ByAgent", + division=self.division, + ) + From d3246497ff413e422df398a08b9aee7fbe166424 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 09:38:11 -0500 Subject: [PATCH 068/126] [test]: create mew issued to agent --- specifyweb/backend/businessrules/tests/test_permit.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 96700e21986..f2f8f43cf5e 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -74,4 +74,13 @@ def test_create_permit_with_agents(self): lastname="ByAgent", division=self.division, ) + # Create new Issued To agent + new_issuedto = models.Agent.objects.create( + agenttype=0, + firstname="Issued", + lastname="ToAgent", + division=self.division, + ) + + From e5815c9f6eba608a5937e8c6aa116856ee1a2c69 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 09:39:00 -0500 Subject: [PATCH 069/126] [test]: create permit with existing agent + new agents --- specifyweb/backend/businessrules/tests/test_permit.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index f2f8f43cf5e..39b26e17dc2 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -81,6 +81,13 @@ def test_create_permit_with_agents(self): lastname="ToAgent", division=self.division, ) + # Create permit with existing agent + new agents + permit = models.Permit.objects.create( + institution=self.institution, + permitnumber='P-AGENTS-001', + issuedby=self.agent, # Issue By (existing agent) + issuedto=new_issuedto, # Issue To (new agent) + ) From 96c7aab28bf14082e93500830cfdf27bf37b8125 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 09:39:45 -0500 Subject: [PATCH 070/126] [test]: verify save + agent relationships --- specifyweb/backend/businessrules/tests/test_permit.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 39b26e17dc2..8d63ccc69b6 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -88,6 +88,13 @@ def test_create_permit_with_agents(self): issuedby=self.agent, # Issue By (existing agent) issuedto=new_issuedto, # Issue To (new agent) ) + # Verify save + agent relationships + self.assertIsNotNone(permit.id) + fetched = models.Permit.objects.get(id=permit.id) + self.assertEqual(fetched.issuedby, self.agent) + self.assertEqual(fetched.issuedby.firstname, 'Test') + self.assertEqual(fetched.issuedto, new_issuedto) + self.assertEqual(fetched.issuedto.firstname, 'Issued') From 653f11a29d52b7d6b75d01c0e2fbd120f1215971 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:10:05 -0500 Subject: [PATCH 071/126] fix: use simpler approach --- .github/workflows/test.yml | 82 +++++++++----------------------------- 1 file changed, 18 insertions(+), 64 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82e9d6320a9..9b3b650923c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -323,6 +323,8 @@ jobs: continue-on-error: true timeout-minutes: 10 + if: github.event_name == 'pull_request' + permissions: pull-requests: write contents: read @@ -340,72 +342,24 @@ jobs: python -m pip install --upgrade pip pip install requests - - name: Run EOL check - id: eol - run: | - output=$(python -m scripts.eol.check_eol || true) - - echo "$output" - - echo "$output" > eol-output.txt - - { - echo "output<> "$GITHUB_OUTPUT" - - - name: Upload EOL results - uses: actions/upload-artifact@v4 + - name: Run EOL check and comment + uses: actions/github-script@v7 with: - name: eol-results - path: eol-output.txt - - eol-comment: - name: Consolidated EOL comment - runs-on: ubuntu-latest - needs: eol-check - - if: github.event_name == 'pull_request' + script: | + const { execSync } = require('child_process'); - permissions: - pull-requests: write - contents: read + const output = execSync('python -m scripts.eol.check_eol 2>&1 || true', { + encoding: 'utf-8', + cwd: process.env.GITHUB_WORKSPACE + }); - steps: - - name: Download EOL artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts + console.log(output); - - name: Combine warnings - id: combine - shell: bash - run: | - combined="" - - while IFS= read -r -d '' file; do - if grep -q '^STATUS=WARNING' "$file"; then - combined="${combined} - $(cat "$file") - " - fi - done < <(find artifacts -name '*.txt' -print0) - - { - echo "warnings<> "$GITHUB_OUTPUT" - - - name: Create or update PR comment - if: steps.combine.outputs.warnings != '' - uses: actions/github-script@v7 - env: - WARNINGS: ${{ steps.combine.outputs.warnings }} - with: - script: | - const warnings = process.env.WARNINGS || ''; + // Only post a comment if there are warnings + if (!output.includes('STATUS=WARNING')) { + console.log('No EOL warnings — skipping comment'); + return; + } const marker = ''; @@ -416,7 +370,7 @@ jobs: '> Please plan upgrades accordingly.\n' + '\n' + '```text\n' + - warnings.replace(/```/g, '``\\`') + + output.replace(/```/g, '``\\`') + '\n```'; const { data: comments } = await github.rest.issues.listComments({ @@ -444,4 +398,4 @@ jobs: issue_number: context.issue.number, body }); - } + } \ No newline at end of file From baede9808392785d01c67ebc53fc222963d8a184 Mon Sep 17 00:00:00 2001 From: Google Translate Date: Wed, 8 Jul 2026 22:03:55 +0000 Subject: [PATCH 072/126] Sync localization strings with Weblate Triggered by 66eea01dc719030e5c8be9ab82f87619594fd749 on branch refs/heads/weblate-localization --- .../frontend/js_src/lib/localization/query.ts | 4 ++-- .../frontend/js_src/lib/localization/wbPlan.ts | 2 +- .../js_src/lib/localization/welcome.ts | 18 +++++++++++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts index 03ba09b5ca1..966827eaef6 100644 --- a/specifyweb/frontend/js_src/lib/localization/query.ts +++ b/specifyweb/frontend/js_src/lib/localization/query.ts @@ -556,11 +556,11 @@ export const queryText = createDictionary({ 'en-us': 'Use "%" to match any number of characters.\n\nUse "_" to match a single character', 'ru-ru': - 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', + 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', 'es-es': 'Usar "%" para hacer coincidir cualquier número de caracteres.\n\nUsar "_" para hacer coincidir un solo carácter', 'fr-fr': - 'Utilisez « % » pour correspondre à un nombre quelconque de caractères.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nUtilisez « _ » pour correspondre à un seul caractère', + 'Utilisez « % » pour correspondre à un nombre quelconque de caractères.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nUtilisez « _ » pour correspondre à un seul caractère', 'uk-ua': 'Використовуйте "%", щоб відповідати будь-якій кількості символів.\n\nВикористовуйте "_", щоб відповідати одному символу', 'de-ch': diff --git a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts index 90242d87b10..c4360a073b5 100644 --- a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts +++ b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts @@ -551,7 +551,7 @@ export const wbPlanText = createDictionary({ 'es-es': 'Está viendo las asignaciones de campos/mapeo para un conjunto de datos ya cargado.\n\nPara editar los mapeos, d´s marcha-atrás para los datos cargados o cree un nuevo conjunto de datos', 'fr-fr': - "Vous visualisez les correspondances d'un jeu de données importé.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPour modifier les correspondances, annulez l'importation des données ou créez un nouveau jeu de données.", + "Vous visualisez les correspondances d'un jeu de données importé.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPour modifier les correspondances, annulez l'importation des données ou créez un nouveau jeu de données.", 'uk-ua': 'Ви переглядаєте зіставлення для завантаженого набору даних.\n\nЩоб редагувати зіставлення, відкотіть завантажені дані або створіть новий набір даних', 'de-ch': diff --git a/specifyweb/frontend/js_src/lib/localization/welcome.ts b/specifyweb/frontend/js_src/lib/localization/welcome.ts index ff6a77212cc..cae8aeaf9ae 100644 --- a/specifyweb/frontend/js_src/lib/localization/welcome.ts +++ b/specifyweb/frontend/js_src/lib/localization/welcome.ts @@ -93,11 +93,27 @@ export const welcomeText = createDictionary({ }, disclosure: { 'en-us': - "Specify is developed by the Specify Collections Consortium (SCC), a collaborative initiative governed by its members and supported by institutional partners. Software support and development are made possible by consortium members, including the UniMus:Natur Consortium of Norway, the Commonwealth Scientific and Industrial Research Organisation (CSIRO), the Consejo Superior de Investigaciones Científicas (CSIC), the Denmark Consortium of Museums, the Muséum d’Histoire Naturelle Geneva, the University of Florida, the University of Kansas, and the University of Michigan, along with numerous other member collections and institutions within the Consortium. The SCC operates under the University of Kansas Center for Research’s non-profit 501(c)(3) U.S. tax status and received support from U.S. National Science Foundation grants from 1996 to 2018.", + 'Specify is developed by the Specify Collections Consortium (SCC), a collaborative initiative governed by its members and supported by institutional partners. Software support and development are made possible by consortium members, including the UniMus:Natur Consortium of Norway, the Commonwealth Scientific and Industrial Research Organisation (CSIRO), the Consejo Superior de Investigaciones Científicas (CSIC), the Denmark Consortium of Museums, the Muséum d’Histoire Naturelle Geneva, the University of Florida, the University of Kansas, and the University of Michigan, along with numerous other member collections and institutions within the Consortium. The SCC operates under the University of Kansas Center for Research’s non-profit 501(c)(3) U.S. tax status and received support from U.S. National Science Foundation grants from 1996 to 2018.', + 'de-ch': '', + 'es-es': '', + 'fr-fr': '', + 'hr-hr': '', + nb: '', + 'pt-br': '', + 'ru-ru': '', + 'uk-ua': '', }, licence: { 'en-us': 'Specify 7, Copyright 2026, University of Kansas Center for Research. Specify comes with ABSOLUTELY NO WARRANTY. This is free, open-source software licensed under GNU General Public License v3.', + 'de-ch': '', + 'es-es': '', + 'fr-fr': '', + 'hr-hr': '', + nb: '', + 'pt-br': '', + 'ru-ru': '', + 'uk-ua': '', }, systemInformation: { 'en-us': 'System Information', From 9b91880253a8244cd578f134b6963e963b30883e Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:14:52 -0500 Subject: [PATCH 073/126] chore: update README --- README.md | 543 +++++++----------------------------------------------- 1 file changed, 70 insertions(+), 473 deletions(-) diff --git a/README.md b/README.md index e1bfaf3cb47..2127fa482cd 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,41 @@ -# [Specify 7](https://www.specifysoftware.org/products/specify-7/) - -The [Specify Collections Consortium](https://www.specifysoftware.org) is pleased -to offer Specify 7, the web implementation of our biological collections data -management platform. - -We encourage members to use our -[Dockerized compositions](https://github.com/specify/docker-compositions) of -Specify 7. You can choose a version, make the necessary adjustments and then run -a single command to get everything working. It is very simple and can be easily -updated when new versions are released. Members can contact us at -[support@specifysoftware.org](mailto:support@specifysoftware.org) to gain access -to this repository. - -The new generation of Specify combines the interface design components and data -management foundation of Specify 6 with the efficiency and ease-of-use of -web-based data access and cloud computing. Specify 7 uses the same interface -layout language as Specify 6, so any user interface customization made in one -platform is mirrored in the other. Also Specify 6 and Specify 7 use the same -data model and can work from the same Specify MySQL or MariaDB database, which -means 6 and 7 can be run simultaneously with any Specify collection. Specify 7 -helps transition Specify 6 collections to cloud computing. It is also a great -starting platform for institutions that prefer zero workstation software -installation and ubiquitous web browser access. - -Specify 7’s architecture supports collaborative digitization projects and remote -hosting of specimen databases. Without the need for a local area or campus -network to connect to the MySQL data server, Specify 7 gives you and your -collaborators access to a shared specimen database through any web browser. -Finding it challenging to obtain IT support to maintain a local secure database -server? With the Specify 7 server software supported on generic Linux servers, -museums can utilize a server hosting service to provide support for the -technical complexities of systems administration, security management, and -backing-up. Want to create a joint database for a collaborative digitizing -effort? No problem! Host, hire a hosting service or use our -[Specify Cloud](https://www.specifysoftware.org/products/cloud/) service for -your Specify database, set up accounts and go. We provide the same efficient -user interface, report and labels customization and help desk support for -Specify 7 as we do for Specify 6. - -**Secure.** Support for Single Sign-On (SSO) integrates Specify 7 with a campus -or institutional identity providers. It supports all identity providers (IdPs) -that have an OpenID endpoints. - -The Security and Accounts tool allows administrators to give access based on -roles and policies. Create, edit, and copy roles among collections and -databases. Administrators can give users as many or few permissions as desired, -from guest accounts to collection managers. - -**Accessible.** It is important that web applications work for people with -disabilities. Specify 7 is developed with this top of mind, not only meeting -international accessibility standards but also providing a better experience for -everyone. - -Specify 7 is largely compliant with the main WWW accessibility standard – **WCAG -2.1 (AA)**. It supports screen readers and allows each user to customize their -color scheme and appearance as well as reduce motion and resize all elements. - -This accessible design respects system and web browser preferences for date -formats, language, theme, and animations. +Specify Collections Consortium ---- +## **Specify Collections Management Platform** + +The [Specify Collections Consortium](https://www.specifysoftware.org) (SCC) is proud to present **Specify 7**, a web-based application for managing biological collections data. Specify supports and enhances data management for biological collections, with a focus on research collections at universities, natural history museums, biorepositories, seed banks, herbaria, and environmental organizations. It offers incredible tools for data management, superb accessibility, and a multitude of collaboration features. It is completely free-to-use and 100% open source, supported by an active community of institutions from around the world. [Click here to learn more about our members](https://www.specifysoftware.org/members/). + +Specify manages species and specimen information to computerize biological collections, track museum specimen transactions, link images to specimen records, and publish catalog data online. It supports collaborative digitization projects and remote hosting of specimen databases. Users do not need to install the software, enabling you and your collaborators to access a shared collections database from any modern web browser. + +You can select, organize, rename, and resize the data entry forms to match your curatorial preferences, eliminating the need to tab through multiple forms. Specify's "tree" interface for taxonomy, geography, storage location, chronostratigraphy, and lithostratigraphy offer intuitive access to hierarchical data. This allows for easy editing, synonymization, re-parenting, and discovering linked collection objects and preparations. + +The platform supports data from specimens, taxonomic and stratigraphic classifications, field notebooks, DNA sequence runs, literature references, and other primary sources. It can manage information related to repository agreements, accessions, conservation treatments, collection object groups, images, and other document attachments. + +Specify prioritizes security with support for Single Sign-On (SSO), integrating seamlessly with institutional identity providers that utilize OpenID endpoints. The Security and Accounts tool empowers administrators to manage user access based on defined roles and policies. You can easily create, edit, and replicate roles across collections and databases, granting users tailored permissions that range from guest access to full collection management. -The Specify Collections Consortium is funded by its member institutions. The -Consortium web site is: https://specifysoftware.org +Specify is developed with accessibility at its core, meeting and exceeding international standards. It largely complies with the WCAG 2.1 (AA) guidelines, ensuring compatibility with screen readers. Users can customize their visual experience by adjusting color schemes, reducing motion, and resizing elements. Our design respects user preferences for date formats, language, themes, and animations, creating a more personalized experience for everyone. -Specify 7 Copyright © 2024 Specify Collections Consortium. Specify comes with -ABSOLUTELY NO WARRANTY. This is free software licensed under GNU General Public -License 3 (GPL3). +To get started, [send us a message to learn more](mailto:membership@specifysoftware.org)! We are happy to meet with you and your team to discuss how we can address your collections data management needs with Specify. - Specify Collections Consortium - Biodiversity Institute - University of Kansas - 1345 Jayhawk Blvd. - Lawrence, KS 66045 USA +> [!IMPORTANT] +> You can learn more about Specify, ask questions, share updates, and get involved with the community on the [Specify Community Forum](https://discourse.specifysoftware.org)! + +--- ## Table of Contents -- [Specify 7](#specify-7) - - [Table of Contents](#table-of-contents) - - [Changelog](#changelog) - [Installation](#installation) - - [Docker Installation (Recommended)](#docker-installation-recommended) - - [Specify Collections Consortium (SCC) Members:](#specify-collections-consortium-scc-members) - - [Non-Members:](#non-members) - - [Local Installation](#local-installation) - - [Installing system dependencies](#installing-system-dependencies) - - [Installing Specify 6](#installing-specify-6) - - [Cloning Specify 7 source repository](#cloning-specify-7-source-repository) - - [Adjusting settings files](#adjusting-settings-files) - - [Setting up Python Virtual Environment](#setting-up-python-virtual-environment) - - [Building](#building) - - [`make build`](#make-build) - - [`make frontend`](#make-frontend) - - [`make clean`](#make-clean) - - [`make pip_requirements`](#make-pip_requirements) - - [`make django_migrations`](#make-django_migrations) - - [`make runserver`](#make-runserver) - - [`make webpack_watch`](#make-webpack_watch) - - [Turning on debugging](#turning-on-debugging) - - [The development server](#the-development-server) - - [The Specify 7 Worker](#the-specify-7-worker) - - [Installing production requirements](#installing-production-requirements) - - [Setting up Apache](#setting-up-apache) - - [Restarting Apache](#restarting-apache) - - [Nginx configuration](#nginx-configuration) - - [Updating Specify 7](#updating-specify-7) - - [Updating the database (Specify 6) version](#updating-the-database-specify-6-version) - - [Localizing Specify 7](#localizing-specify-7) - -## Changelog - -Changelog is available in [CHANGELOG.md](./CHANGELOG.md) + - [Specify Cloud (Recommended)](#specify-cloud-recommended) + - [Self-hosted (Docker)](#self-hosted-docker) + - [Development Setup](#development-setup) +- [Tech Stack](#tech-stack) +- [Specify Components](#specify-components) + - [Specify 7](#specify-7) + - [Specify Worker](#specify-worker) + - [Specify Asset Server](#specify-asset-server) + - [Specify Report Runner](#specify-report-runner) + - [Localizing Specify](#localizing-specify) + +--- # Installation @@ -118,404 +43,76 @@ We encourage all users to read our documentation on the Community Forum regarding installing and deploying Specify – [**Specify 7 Installation Instructions**](https://discourse.specifysoftware.org/t/specify-7-installation-instructions/755). -If you are an existing Specify 6 user who is looking to evaluate Specify 7, you -can contact [support@specifysoftware.org](mailto:support@specifysoftware.org) +If you are an existing Specify 6 user who is looking to evaluate Specify 7, you can +contact [support@specifysoftware.org](mailto:support@specifysoftware.org) along with a copy of your database and we can configure a temporary deployment for evaluation purposes. -## Docker Installation (Recommended) - -### Specify Collections Consortium (SCC) Members: - -We encourage members to use our -[Dockerized compositions](https://github.com/specify/docker-compositions) of -Specify 7. You can choose your desired version, make the necessary adjustments -and then run a single command to get everything working. It is very simple and -can be easily updated when new versions are released. Documentation for -deploying Specify using Docker is available within the repository. - -[**📨 Click here to request access**](mailto:support@specifysoftware.org?subject=Requesting%20Docker%20Repository%20Access&body=My%20GitHub%20username%20is%3A%20%0D%0AMy%20Specify%20Member%20Institution%20is%3A%20%0D%0AAdditional%20Questions%20or%20Notes%3A%20) -or email [support@specifysoftware.org](mailto:support@specifysoftware.org) with -your GitHub username, member institution or collection, and any additional -questions you have for us. - -### Non-Members: - -If your institution is not a member of the Specify Collections Consortium, you -can follow the [local installation instructions](#local-installation) below or -contact [membership@specifysoftware.org](mailto:membership@specifysoftware.org) -to learn more about joining the SCC to receiving configuration assistance, -support, and hosting services if you are interested. - -## Local Installation - -After completing these instructions you will be able to run the test server and -interact with the Django based Specify webapp in your browser on your local -machine. - -Instructions for deployment follow. - -**Note:** If updating from a previous version, some of the python dependencies -have changed. It is recommended to place the new version in a separate directory -next to the previous version and install all the new dependencies in a Python -virtualenv as described below. That will avoid version conflicts and allow the -previous version to continue working while the new version is being set up. When -the new version is working satisfactorily using the test server, the Apache conf -can be changed to point to it (or changed back to the old version, if problems -arise). - -### Installing system dependencies - -Specify 7 requires Python 3.8. Ubuntu 20.04 LTS is recommended. For other -distributions these instructions will have to be adapted. - -Ubuntu 20.04 LTS: - -```shell -sudo apt install -y curl -curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - -sudo apt-get -y install --no-install-recommends \ - build-essential \ - git \ - libldap2-dev \ - libmariadbclient-dev \ - libsasl2-dev \ - nodejs \ - python3-venv \ - python3.8 \ - python3.8-dev \ - redis \ - unzip -``` - -CentOS 7 / Red Hat 7: - -```shell -yum install -y epel-release sudo wget -yum install -y \ - gcc make \ - git \ - openldap-devel \ - mariadb-devel \ - nodejs \ - npm \ - java-11-openjdk-headless \ - python36-virtualenv \ - python36 \ - python36u-devel \ - redis \ - unzip -``` - -Afterward, please make sure you have Node.js 20 installed: - -``` -node -v -``` - -### Installing Specify 6 - -A copy of the most recent Specify 6 release is required on the server as Specify -7 makes use of resource files. A Java runtime is required to execute the Specify -6 installer, but is not needed to run Specify 7. It is possible to copy the -Specify 6 install from another Linux system to avoid the need to install Java on -the server. - -```shell -wget https://update.specifysoftware.org/Specify_unix_64.sh -sh Specify_unix_64.sh -q -dir ./Specify6.8.03 -sudo ln -s $(pwd)/Specify6.8.03 /opt/Specify -``` - -### Cloning Specify 7 source repository - -Clone this repository. - -```shell -git clone https://github.com/specify/specify7.git -``` - -You will now have a specify7 directory containing the source tree. - -Note, by default, `git clone` checks out the `main` branch of Specify 7. -That branch contains the latest tested features and bug fixes. If you prefer a -more stable release, you can switch to one of our tagged released. - -```shell -cd specify7 -git checkout tags/v7.8.6 -``` - -Tagged releases are coming out every other week and undergo more testing. - -See [the list of tags](https://github.com/specify/specify7/tags) to check what's -the latest stable release. - -### Adjusting settings files - -In the directory `specify7/specifyweb/settings` you will find the -`specify_settings.py` file. Make a copy of this file as -`local_specify_settings.py` and edit it. The file contains comments explaining -the various settings. - -### Setting up Python Virtual Environment - -Using a Python -[virtual environment](https://docs.python-guide.org/en/latest/dev/virtualenvs/) -will avoid version conflicts with other Python libraries on your system. Also, -it avoids having to use a superuser account to install the Python dependencies. - -```shell -python3.8 -m venv specify7/ve -specify7/ve/bin/pip install wheel -specify7/ve/bin/pip install --upgrade -r specify7/requirements.txt -``` - -### Building - -To build Specify 7 use the default make target. - -```shell -cd specify7 -source ve/bin/activate -make -``` - -> Note, if `source` command is not available on your system, try running -> `. ve/bin/activate` instead - -Other make targets: - -#### `make build` - -Runs all necessary build steps. - -#### `make frontend` - -Installs or updates Javascript dependencies and builds the Javascript modules -only. - -#### `make clean` - -Removes all generated files. - -The following targets require the virualenv to be activated: +## Specify Cloud (Recommended) -#### `make pip_requirements` +Our hosting platform, Specify Cloud, enables biological collections to easily share their current Specify database with us. If a collection is new to Specify, we are happy to create a new database [upon request](mailto:membership@specifysoftware.org)! We handle all updates, maintenance, backups, resource management, billing, and asset management. Our cloud platform has regions located worldwide, enabling Specify to be hosted near your collection and staff. We will collaborate with your team to ensure compliance with all institutional and legal regulations regarding data storage and accessibility. Your local IT teams can request access to our cloud servers to prepare backups, access assets, and connect to your database directly whenever necessary. This platform also enables our support team to respond to inquiries quickly and resolve any issues. -Install or updates Python dependencies. +## Self-hosted (Docker) -#### `make django_migrations` +For institutions that require their collections data to remain on-site or in a region not served by Specify Cloud, Specify 7 is designed to be deployed using [Docker](https://www.docker.com/). The project provides a [Dockerfile](./Dockerfile) based on Ubuntu and [docker-compositions](https://github.com/specify/docker-compositions) that bundle Specify 7 with its supporting services (MariaDB, Redis, Asset Server, Report Runner). This is the only supported production self-hosted deployment method. -Applies Specify schema changes to the database named in the settings. This step -may fail if the master user configured in the settings does not have DDL -privileges. Changing the `MASTER_NAME` and `MASTER_PASSWORD` settings to the -MySQL root user will allow the changes to be applied. Afterward, the master user -settings can be restored. +We encourage SCC members to use our [Dockerized compositions](https://github.com/specify/docker-compositions) of Specify 7. You can choose your desired version, make the necessary adjustments and then run a single command to get everything working. It is very simple and can be easily updated when new versions are released. Documentation for deploying Specify using Docker is available within the repository. -#### `make runserver` +[**📨 Click here to request access**](mailto:support@specifysoftware.org?subject=Requesting%20Docker%20Repository%20Access&body=My%20GitHub%20username%20is%3A%20%0D%0AMy%20Specify%20Member%20Institution%20is%3A%20%0D%0AAdditional%20Questions%20or%20Notes%3A%20), including your GitHub username, member institution, collection, and any additional questions or notes you have for us. -A shortcut for running the Django development server. +## Development Setup -#### `make webpack_watch` +For contributing to Specify 7, the recommended approach is a [Docker-based development workflow](https://discourse.specifysoftware.org/t/guide-to-contributing-code-to-specify/3511). The included [Dockerfile](./Dockerfile) provides a multi-stage build with a dedicated `run-development` stage that includes testing dependencies and tools like mypy. If you are interested in contributing to Specify, please read our [guide to contributing code to Specify](run-development) for instructions! -Run webpack in watch mode so that changes to the frontend source code will be -automatically compiled. Useful during the development process. +If you want to contribute to our code from an external institution, please reach out to a [member of our team](mailto:support@specifysoftware.org) for further guidance. We are always looking for new collaboration opportunities. -### Turning on debugging - -For development purposes, Django debugging should be turned on. It will enable -stack traces in responses that encounter exceptions, and allow operation with -the unoptimized Javascript files. - -Debugging can be enabled by creating the file -`specify7/specifyweb/settings/debug.py` with the contents, `DEBUG = True`. - -### The development server - -> NOTE: development server should only be run in debug mode. See previous -> section for instructions on how to turn on debugging. - -Specify7 can be run using the Django development server. - -```shell -cd specify7 -source ve/bin/activate -make runserver -``` - -This will start a development server for testing purposes on `localhost:8000`. - -When the server starts up, it will issue a warning that some migrations have not -been applied: - -``` -You have 11 unapplied migration(s). Your project may not work -properly until you apply the migrations for app(s): auth, -contenttypes, sessions. Run 'python manage.py migrate' to apply them. -``` - -Specify 7 makes use of functions from the listed Django apps (auth, -contenttypes, and sessions) but does not need the corresponding tables to be -added to the database. Running `make django_migrations` will apply only those -migrations needed for Specify 7 to operate. - -### The Specify 7 Worker - -Starting from version `v7.6.0`, the Specify WorkBench utilizes this dedicated -worker process to handle the upload and validation operations. - -Starting from version `v7.9.0`, the record merging functionality employs the -worker to handle all record merging activities. - -This worker process utilizes -[Celery](https://docs.celeryproject.org/en/master/index.html), a job queue -management system, with -[Redis](https://docs.celeryproject.org/en/master/getting-started/backends-and-brokers/redis.html) -serving as the broker. - -The worker process can be started from the commandline by executing: - -```shell -cd specify7 -celery -A specifyweb worker -l INFO --concurrency=1 -``` - -For deployment purposes it is recommended to configure a systemd unit to -automatically start the Specify 7 worker process on system start up by executing -the above command within the installation directory. It is possible to run Redis -and worker process on a separate server and to provision multiple worker -processes for high volume scenarios. Contact the Specify team about these use -cases. - -### Installing production requirements - -For production environments, Specify7 can be hosted by Apache. The following -packages are needed: - -- Apache -- mod-wsgi to connect Python to Apache - -Ubuntu: - -```shell -sudo apt-get install apache2 libapache2-mod-wsgi-py3 -``` - -CentOS / Red Hat: - -```shell -yum install httpd python3-mod_wsgi -``` - -Warning: This will replace the Python 2.7 version of mod-wsgi that was used by -Specify 7.4.0 and prior. If executed on a production server running one of those -versions, Specify 7 will stop working until the new deployment is configured. - -### Setting up Apache - -In the `specify7` directory you will find the `specifyweb_apache.conf` file. -Make a copy of the file as `local_specifyweb_apache.conf` and edit the contents -to reflect the location of Specify6 and Specify7 on your system. There are -comments showing what to change. - -Then remove the default Apache welcome page and make a link to your -`local_specifyweb_apache.conf` file. - -Ubuntu: - -```shell -sudo rm /etc/apache2/sites-enabled/000-default.conf -sudo ln -s $(pwd)/specify7/local_specifyweb_apache.conf /etc/apache2/sites-enabled/ -``` - -CentOS / Red Hat: - -```shell -sudo ln -s $(pwd)/specify7/local_specifyweb_apache.conf /etc/httpd/conf.d/ -``` - -### Restarting Apache - -After changing Apache's config files restart the service. - -Ubuntu: - -```shell -sudo systemctl restart apache2.service -``` - -CentOS / Red Hat: - -```shell -sudo systemctl restart httpd.service -``` - -### Nginx configuration +--- -Specify 7 is web-server agnostic. Example -[nginx.conf](https://github.com/specify/specify7/blob/main/nginx.conf) -(note, you would have to adjust the host names and enable HTTPs). +# Tech Stack -## Updating Specify 7 +- **Host:** Ubuntu on Docker +- **Database Management System:** MariaDB +- **Front-end:** TypeScript, React, JavaScript, Tailwind CSS +- **Back-end:** Django and Python -Specify 7.4.0 and prior versions were based on Python 2.7. If updating from one -of these versions, it will be necessary to install Python 3.8 by running the -`apt-get` commands in the -[Install system dependencies](#install-system-dependencies) and the -[Production requirements](#production-requirements) steps. Then proceed as -follows: +You can learn more about the architecture here: https://discourse.specifysoftware.org/t/specify-7-architecture-overview/ -0. Backup your Specify database using MySQL dump or the Specify backup and - restore tool. +--- -1. Clone or download a new copy of this repository in a directory next to your - existing installation. +# Specify Components - `git clone https://github.com/specify/specify7.git specify7-new-version` +A full Specify 7 deployment consists of several services working together. The Docker compositions bundle all of these into a single, cohesive container stack. -2. Copy the settings from the existing to the new installation. +## Specify 7 - `cp specify7/specifyweb/settings/local* specify7-new-version/specifyweb/settings/` +The main web application — a Django-powered backend serving a React and TypeScript frontend. This is the browser-based interface that users interact with to manage their collections data. It handles authentication (including SSO via OpenID Connect), business logic, schema management, and all CRUD operations against the MariaDB database. The application is served via Gunicorn in production and includes a built-in development server for local testing. -3. Make sure to update the `THICK_CLIENT_LOCATION` setting in - `local_specify_settings.py`, if you are updating the Specify 6 version. +## Specify Worker -4. Update the system level dependencies by executing the _apt-get_ command in - the [Installing system dependencies](#installing-system-dependencies) - section. +A background job processor that handles long-running or resource-intensive operations asynchronously, keeping the main web application responsive. It is responsible for: -5. Create a new virtualenv for the new installation by following the - [Python Virtual Environment](#python-virtual-environment) section for the new - directory. +- **WorkBench uploads** — processing and validating large datasets uploaded through the Specify WorkBench tool +- **Record merging** — executing complex record merge operations across related tables +- **Batch attachment uploads** — handling bulk image and document attachment processing -6. [Build](#building) the new version of Specify 7. +The worker process is built on [Celery](https://docs.celeryproject.org/en/master/index.html), a distributed task queue, with [Redis](https://docs.celeryproject.org/en/master/getting-started/backends-and-brokers/redis.html) serving as the message broker. It is included alongside every Specify 7 deployment. -7. Test it out with the [development server](#the-development-server). +## Specify Asset Server -8. Deploy the new version by updating your Apache config to replace the old - installation paths with the new ones and restarting Apache. +The [Web Asset Server](https://discourse.specifysoftware.org/t/specify-web-asset-server-attachment-server/715) manages the storage and retrieval of binary file attachments such as specimen images, scanned documents, field notes, and other media files linked to collection records. It stores assets independently from the main application database, enabling efficient serving of large files without impacting database performance. The Asset Server supports configurable storage backends and collection-based organization of assets. -9. Configure the Specify 7 worker process to execute at system start up as - described in [The Specify 7 worker](#the-specify-7-worker) section. +* Learn about [Attachments in Specify](https://discourse.specifysoftware.org/t/attachments-in-specify/190) +* See how [Specify works with the Web Asset Server](https://discourse.specifysoftware.org/t/how-does-specify-7-work-with-the-web-asset-server/2817) +* Read the [technical docs for the Web Asset Server](https://discourse.specifysoftware.org/t/web-asset-server/657) -## Updating the database (Specify 6) version +## Specify Report Runner -The Specify database is updated from one version to the next by the Specify 6 -application. To update the database version connect to the database with a new -version of Specify 6 and follow the Specify 6 update procedures. +The [Report Runner Service](https://github.com/specify/report-runner-service) generates formatted, printable reports from Specify data. It processes report templates against collection records to produce PDFs of invoices and labels. This service handles the rendering pipeline independently so that complex report generation does not block the main application. If you are interested in adding additional fonts for use in reports and labels, see [these instructions here](https://discourse.specifysoftware.org/t/specify-report-runner-fonts/1659). -Once the database version is updated, a corresponding copy of Specify 6 must be -provided to the Specify 7 server by repeating the -[Installing Specify 6](#installing-specify-6) section of this guide for the new -version of Specify 6. +## Localizing Specify -[![analytics](https://www.google-analytics.com/collect?v=1&t=pageview&dl=https%3A%2F%2Fgithub.com%2Fspecify%2Fspecify7&uid=readme&tid=UA-169822764-3)]() +Specify 7 interface is available in several languages out of the box, including English, Ukrainian, Russian, German, and French. We are using [Weblate](https://hosted.weblate.org/projects/specify-7/) continuous localization platform, and if you are interested in amending our existing localization or would like us to add a new language to Specify, please see our [instructions on how you can contribute](https://discourse.specifysoftware.org/t/get-started-with-specify-7-localization/956). -## Localizing Specify 7 +--- -Specify 7 interface is localized to a few languages out of the box. We welcome -contributions of new translations. We are using -[Weblate](https://hosted.weblate.org/projects/specify-7/) continuous -localization platform. -[Instructions on how you can contribute](https://discourse.specifysoftware.org/t/get-started-with-specify-7-localization/956) +_The Specify Collections Consortium is funded by its member institutions. Find out more at [https://specifysoftware.org](https://specifysoftware.org)_ \ No newline at end of file From 73e94011c80e38dd6d4e39c83c001d82c4283107 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:16:52 -0500 Subject: [PATCH 074/126] chore: add data ownership mention --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2127fa482cd..612fd80347e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Specify prioritizes security with support for Single Sign-On (SSO), integrating Specify is developed with accessibility at its core, meeting and exceeding international standards. It largely complies with the WCAG 2.1 (AA) guidelines, ensuring compatibility with screen readers. Users can customize their visual experience by adjusting color schemes, reducing motion, and resizing elements. Our design respects user preferences for date formats, language, themes, and animations, creating a more personalized experience for everyone. +Specify 7's web-based architecture supports collaborative digitization projects across institutions and enables remote hosting of specimen databases, eliminating the need for campus VPNs or local network access to connect to your data. Whether you choose to self-host on your own infrastructure or use our [Specify Cloud](https://www.specifysoftware.org/products/cloud/) service, **your institution retains full ownership and control of your data at all times.** Specify imposes no vendor lock-in — your database runs on standard MariaDB (MySQL-compatible), can be backed up or migrated at any time, and remains entirely yours. If your institution has strict data sovereignty or on-premises hosting requirements, the [self-hosted Docker deployment](#self-hosted-docker) puts you in complete control of your infrastructure, security policies, and data access. + To get started, [send us a message to learn more](mailto:membership@specifysoftware.org)! We are happy to meet with you and your team to discuss how we can address your collections data management needs with Specify. > [!IMPORTANT] From 37274f1eaacfb37e3874085d682336b1ad35e4d4 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Wed, 8 Jul 2026 16:20:52 -0500 Subject: [PATCH 075/126] fix: add podman --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 612fd80347e..e079bc204e4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Specify prioritizes security with support for Single Sign-On (SSO), integrating Specify is developed with accessibility at its core, meeting and exceeding international standards. It largely complies with the WCAG 2.1 (AA) guidelines, ensuring compatibility with screen readers. Users can customize their visual experience by adjusting color schemes, reducing motion, and resizing elements. Our design respects user preferences for date formats, language, themes, and animations, creating a more personalized experience for everyone. -Specify 7's web-based architecture supports collaborative digitization projects across institutions and enables remote hosting of specimen databases, eliminating the need for campus VPNs or local network access to connect to your data. Whether you choose to self-host on your own infrastructure or use our [Specify Cloud](https://www.specifysoftware.org/products/cloud/) service, **your institution retains full ownership and control of your data at all times.** Specify imposes no vendor lock-in — your database runs on standard MariaDB (MySQL-compatible), can be backed up or migrated at any time, and remains entirely yours. If your institution has strict data sovereignty or on-premises hosting requirements, the [self-hosted Docker deployment](#self-hosted-docker) puts you in complete control of your infrastructure, security policies, and data access. +Whether you choose to self-host on your own infrastructure or use our [Specify Cloud](https://www.specifysoftware.org/products/cloud/) service, your institution retains full ownership and control of your data at all times. Specify imposes no vendor lock-in as your database runs on standard MariaDB, can be backed up or migrated at any time, and remains entirely yours. If your institution has strict data sovereignty or on-premises hosting requirements, the [self-hosted Docker/Podman deployment](#self-hosted-docker--podman) puts you in complete control of your infrastructure, security policies, and data access. To get started, [send us a message to learn more](mailto:membership@specifysoftware.org)! We are happy to meet with you and your team to discuss how we can address your collections data management needs with Specify. @@ -27,7 +27,7 @@ To get started, [send us a message to learn more](mailto:membership@specifysoftw - [Installation](#installation) - [Specify Cloud (Recommended)](#specify-cloud-recommended) - - [Self-hosted (Docker)](#self-hosted-docker) + - [Self-hosted (Docker / Podman)](#self-hosted-docker--podman) - [Development Setup](#development-setup) - [Tech Stack](#tech-stack) - [Specify Components](#specify-components) @@ -54,17 +54,17 @@ for evaluation purposes. Our hosting platform, Specify Cloud, enables biological collections to easily share their current Specify database with us. If a collection is new to Specify, we are happy to create a new database [upon request](mailto:membership@specifysoftware.org)! We handle all updates, maintenance, backups, resource management, billing, and asset management. Our cloud platform has regions located worldwide, enabling Specify to be hosted near your collection and staff. We will collaborate with your team to ensure compliance with all institutional and legal regulations regarding data storage and accessibility. Your local IT teams can request access to our cloud servers to prepare backups, access assets, and connect to your database directly whenever necessary. This platform also enables our support team to respond to inquiries quickly and resolve any issues. -## Self-hosted (Docker) +## Self-hosted (Docker / Podman) -For institutions that require their collections data to remain on-site or in a region not served by Specify Cloud, Specify 7 is designed to be deployed using [Docker](https://www.docker.com/). The project provides a [Dockerfile](./Dockerfile) based on Ubuntu and [docker-compositions](https://github.com/specify/docker-compositions) that bundle Specify 7 with its supporting services (MariaDB, Redis, Asset Server, Report Runner). This is the only supported production self-hosted deployment method. +For institutions that require their collections data to remain on-site or in a region not served by Specify Cloud, Specify 7 is designed to be deployed using [Docker](https://www.docker.com/) or [Podman](https://podman.io/). The project provides a [Dockerfile](./Dockerfile) based on Ubuntu and [docker-compositions](https://github.com/specify/docker-compositions) that bundle Specify 7 with its supporting services (MariaDB, Redis, Asset Server, Report Runner). This is the only supported production self-hosted deployment method. -We encourage SCC members to use our [Dockerized compositions](https://github.com/specify/docker-compositions) of Specify 7. You can choose your desired version, make the necessary adjustments and then run a single command to get everything working. It is very simple and can be easily updated when new versions are released. Documentation for deploying Specify using Docker is available within the repository. +We encourage SCC members to use our [Dockerized compositions](https://github.com/specify/docker-compositions) of Specify 7. You can choose your desired version, make the necessary adjustments and then run a single command to get everything working. It is very simple and can be easily updated when new versions are released. Documentation for deploying Specify using Docker/Podman is available within the repository. [**📨 Click here to request access**](mailto:support@specifysoftware.org?subject=Requesting%20Docker%20Repository%20Access&body=My%20GitHub%20username%20is%3A%20%0D%0AMy%20Specify%20Member%20Institution%20is%3A%20%0D%0AAdditional%20Questions%20or%20Notes%3A%20), including your GitHub username, member institution, collection, and any additional questions or notes you have for us. ## Development Setup -For contributing to Specify 7, the recommended approach is a [Docker-based development workflow](https://discourse.specifysoftware.org/t/guide-to-contributing-code-to-specify/3511). The included [Dockerfile](./Dockerfile) provides a multi-stage build with a dedicated `run-development` stage that includes testing dependencies and tools like mypy. If you are interested in contributing to Specify, please read our [guide to contributing code to Specify](run-development) for instructions! +For contributing to Specify 7, the recommended approach is a [Docker/Podman-based development workflow](https://discourse.specifysoftware.org/t/guide-to-contributing-code-to-specify/3511). The included [Dockerfile](./Dockerfile) provides a multi-stage build with a dedicated `run-development` stage that includes testing dependencies and tools like mypy. If you are interested in contributing to Specify, please read our [guide to contributing code to Specify](run-development) for instructions! If you want to contribute to our code from an external institution, please reach out to a [member of our team](mailto:support@specifysoftware.org) for further guidance. We are always looking for new collaboration opportunities. @@ -72,7 +72,7 @@ If you want to contribute to our code from an external institution, please reach # Tech Stack -- **Host:** Ubuntu on Docker +- **Host:** Ubuntu on Docker / Podman - **Database Management System:** MariaDB - **Front-end:** TypeScript, React, JavaScript, Tailwind CSS - **Back-end:** Django and Python @@ -83,7 +83,7 @@ You can learn more about the architecture here: https://discourse.specifysoftwar # Specify Components -A full Specify 7 deployment consists of several services working together. The Docker compositions bundle all of these into a single, cohesive container stack. +A full Specify 7 deployment consists of several services working together. The Docker / Podman compositions bundle all of these into a single, cohesive container stack. ## Specify 7 From 95e36c1f53ca95915c29cc5929c29c190c0d2d52 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:32:01 -0500 Subject: [PATCH 076/126] fix: add links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e079bc204e4..6422ef04214 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The platform supports data from specimens, taxonomic and stratigraphic classific Specify prioritizes security with support for Single Sign-On (SSO), integrating seamlessly with institutional identity providers that utilize OpenID endpoints. The Security and Accounts tool empowers administrators to manage user access based on defined roles and policies. You can easily create, edit, and replicate roles across collections and databases, granting users tailored permissions that range from guest access to full collection management. -Specify is developed with accessibility at its core, meeting and exceeding international standards. It largely complies with the WCAG 2.1 (AA) guidelines, ensuring compatibility with screen readers. Users can customize their visual experience by adjusting color schemes, reducing motion, and resizing elements. Our design respects user preferences for date formats, language, themes, and animations, creating a more personalized experience for everyone. +Specify is developed with accessibility at its core, meeting and exceeding international standards. It largely complies with the WCAG 2.1 (AA) guidelines, ensuring compatibility with screen readers. You can read more in our [Accessibility & Compatibility Summary (WCAG 2.1 Level AA)](https://discourse.specifysoftware.org/t/accessibility-compatibility-summary-wcag-2-1-level-aa/2870). Users can customize their visual experience by adjusting color schemes, reducing motion, and resizing elements. Our design respects user preferences for date formats, language, themes, and animations, creating a more personalized experience for everyone. Whether you choose to self-host on your own infrastructure or use our [Specify Cloud](https://www.specifysoftware.org/products/cloud/) service, your institution retains full ownership and control of your data at all times. Specify imposes no vendor lock-in as your database runs on standard MariaDB, can be backed up or migrated at any time, and remains entirely yours. If your institution has strict data sovereignty or on-premises hosting requirements, the [self-hosted Docker/Podman deployment](#self-hosted-docker--podman) puts you in complete control of your infrastructure, security policies, and data access. @@ -64,7 +64,7 @@ We encourage SCC members to use our [Dockerized compositions](https://github.com ## Development Setup -For contributing to Specify 7, the recommended approach is a [Docker/Podman-based development workflow](https://discourse.specifysoftware.org/t/guide-to-contributing-code-to-specify/3511). The included [Dockerfile](./Dockerfile) provides a multi-stage build with a dedicated `run-development` stage that includes testing dependencies and tools like mypy. If you are interested in contributing to Specify, please read our [guide to contributing code to Specify](run-development) for instructions! +For contributing to Specify 7, the recommended approach is a [Docker/Podman-based development workflow](https://discourse.specifysoftware.org/t/guide-to-contributing-code-to-specify/3511). The included [Dockerfile](./Dockerfile) provides a multi-stage build with a dedicated `run-development` stage that includes testing dependencies and tools like mypy. If you are interested in contributing to Specify, please read our [guide to contributing code to Specify](https://discourse.specifysoftware.org/t/guide-to-contributing-code-to-specify/3511) for instructions! If you want to contribute to our code from an external institution, please reach out to a [member of our team](mailto:support@specifysoftware.org) for further guidance. We are always looking for new collaboration opportunities. From 1faf19920401dca2ac346acee500af65d7daca00 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:47:54 +0000 Subject: [PATCH 077/126] Lint code with ESLint and Prettier Triggered by d0835b3d52c54de81ff9f7484d9c64d5d13d83a1 on branch refs/heads/issue-8262 --- .../js_src/lib/components/DataModel/tables.ts | 14 ++++++-------- .../__tests__/biostratConditions.test.ts | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/tables.ts b/specifyweb/frontend/js_src/lib/components/DataModel/tables.ts index 93c29044ac6..32d4bfbafb4 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/tables.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/tables.ts @@ -111,10 +111,9 @@ export const fetchContext = f // Then process fields and relationships for each table. tablePairs.forEach(([tableDefinition, table]) => { - const [frontEndFields, callback] = - (schemaExtras[table.name] as (typeof schemaExtras)['Agent'] | undefined)?.( - table as SpecifyTable - ) ?? [[]]; + const [frontEndFields, callback] = ( + schemaExtras[table.name] as (typeof schemaExtras)['Agent'] | undefined + )?.(table as SpecifyTable) ?? [[]]; const [literalFields, relationships] = split( frontEndFields.map((field) => { field.isReadOnly = true; @@ -155,10 +154,9 @@ export const fetchContext = f Object.fromEntries(table.fields.map((field) => [field.name, field])) ); - frontEndOnlyFields[table.name] = [ - ...literalFields, - ...relationships, - ].map(({ name }) => name); + frontEndOnlyFields[table.name] = [...literalFields, ...relationships].map( + ({ name }) => name + ); callback?.(); }); diff --git a/specifyweb/frontend/js_src/lib/components/QueryComboBox/__tests__/biostratConditions.test.ts b/specifyweb/frontend/js_src/lib/components/QueryComboBox/__tests__/biostratConditions.test.ts index 162a8775c36..38a5a61fd27 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryComboBox/__tests__/biostratConditions.test.ts +++ b/specifyweb/frontend/js_src/lib/components/QueryComboBox/__tests__/biostratConditions.test.ts @@ -68,4 +68,4 @@ describe('getQueryComboBoxConditions with BioStrat typeSearchName', () => { serialized.filter(({ fieldName }) => fieldName === 'isBioStrat') ).toHaveLength(0); }); -}); \ No newline at end of file +}); From f11db9119168e64d0ef0d7b171b297769ca4ef03 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 9 Jul 2026 17:31:52 +0000 Subject: [PATCH 078/126] Sync localization strings with Weblate Triggered by 58c63f09140e1cd4a1f95203d372f918e7213b85 on branch refs/heads/weblate-localization --- .../frontend/js_src/lib/localization/query.ts | 2 +- .../js_src/lib/localization/setupTool.ts | 5 +- .../js_src/lib/localization/welcome.ts | 6 +- .../js_src/lib/localization/workbench.ts | 416 +++++++++--------- 4 files changed, 227 insertions(+), 202 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts index 966827eaef6..de64debcd30 100644 --- a/specifyweb/frontend/js_src/lib/localization/query.ts +++ b/specifyweb/frontend/js_src/lib/localization/query.ts @@ -556,7 +556,7 @@ export const queryText = createDictionary({ 'en-us': 'Use "%" to match any number of characters.\n\nUse "_" to match a single character', 'ru-ru': - 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', + 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', 'es-es': 'Usar "%" para hacer coincidir cualquier número de caracteres.\n\nUsar "_" para hacer coincidir un solo carácter', 'fr-fr': diff --git a/specifyweb/frontend/js_src/lib/localization/setupTool.ts b/specifyweb/frontend/js_src/lib/localization/setupTool.ts index 3c6195443fa..b942d5936a5 100644 --- a/specifyweb/frontend/js_src/lib/localization/setupTool.ts +++ b/specifyweb/frontend/js_src/lib/localization/setupTool.ts @@ -496,7 +496,7 @@ export const setupToolText = createDictionary({ 'hr-hr': '"Je u punom imenu" uključuje razinu prilikom izrade izraza punog imena, koji se može upitati i koristiti u izvješćima.', 'pt-br': - '"Está no nome completo" inclui o nível ao construir uma expressão de nome completo, que pode ser consultada e usada em relatórios.', + 'A opção "Nome Completo" inclui o nível de detalhamento ao construir uma expressão de nome completo, que pode ser consultada e usada em relatórios.', 'ru-ru': 'Функция «Указание полного имени» включает уровень детализации при построении выражения для полного имени, которое можно запрашивать и использовать в отчетах.', 'uk-ua': @@ -831,7 +831,8 @@ export const setupToolText = createDictionary({ 'hr-hr': 'Kliknite bilo koji blok da biste otvorili njegov obrazac za uređivanje.', 'pt-br': 'Clique em qualquer bloco para abrir o formulário de edição.', - 'ru-ru': 'Нажмите на любой блок, чтобы открыть форму его редактирования.', + 'ru-ru': + 'Щелкните по любому блоку, чтобы открыть форму его редактирования.', 'uk-ua': 'Клацніть будь-який блок, щоб відкрити його форму редагування.', nb: 'Klikk på en hvilken som helst blokk for å åpne redigeringsskjemaet.', }, diff --git a/specifyweb/frontend/js_src/lib/localization/welcome.ts b/specifyweb/frontend/js_src/lib/localization/welcome.ts index cae8aeaf9ae..6586d2559a8 100644 --- a/specifyweb/frontend/js_src/lib/localization/welcome.ts +++ b/specifyweb/frontend/js_src/lib/localization/welcome.ts @@ -97,9 +97,11 @@ export const welcomeText = createDictionary({ 'de-ch': '', 'es-es': '', 'fr-fr': '', - 'hr-hr': '', + 'hr-hr': + "Specify softver je proizvod konzorcija Specify Collections kojim upravljaju i financiraju ga njegove institucije članice. Osnivači konzorcija uključuju: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Danski konzorcij muzeja, Muséum d'Histoire Naturelle Geneva, Sveučilište Florida, Sveučilište Kansas i Sveučilište Michigan. Konzorcij djeluje pod neprofitnim, poreznim statusom 501(c)3 Sveučilišnog centra za istraživanje u Kansasu. Specify je od 1996. do 2018. godine financiran bespovratnim sredstvima američke Nacionalne zaklade za znanost.", nb: '', - 'pt-br': '', + 'pt-br': + "O software Specify é um produto do Specify Collections Consortium, que é governado e financiado por suas instituições membros. Os membros fundadores do Consórcio incluem: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Denmark Consortium of Museums, Muséum d'Histoire Naturelle Geneva, University of Florida, University of Kansas e University of Michigan. O Consórcio opera sob o status de organização sem fins lucrativos, 501(c)3, do Centro de Pesquisa da Universidade do Kansas, nos EUA. O Specify foi financiado de 1996 a 2018 por bolsas da Fundação Nacional de Ciência dos EUA (NSF).", 'ru-ru': '', 'uk-ua': '', }, diff --git a/specifyweb/frontend/js_src/lib/localization/workbench.ts b/specifyweb/frontend/js_src/lib/localization/workbench.ts index 6d890abb3b7..e233634f091 100644 --- a/specifyweb/frontend/js_src/lib/localization/workbench.ts +++ b/specifyweb/frontend/js_src/lib/localization/workbench.ts @@ -23,13 +23,13 @@ export const wbText = createDictionary({ rollback: { 'en-us': 'Rollback', 'ru-ru': 'Откат', - 'es-es': 'Retroceder', + 'es-es': 'Revertir', 'fr-fr': 'Retour en arrière', 'uk-ua': 'Відкат', - 'de-ch': 'Zurückrollen', + 'de-ch': 'Rückgängigmachen', 'pt-br': 'Reverter', - 'hr-hr': 'Vrati se', - nb: 'Rull tilbake', + 'hr-hr': 'Vraćanje na prethodno stanje', + nb: 'Tilbakerulling', }, validate: { 'en-us': 'Validate', @@ -66,14 +66,14 @@ export const wbText = createDictionary({ }, rollingBack: { 'en-us': 'Rolling back', - 'ru-ru': 'Откат', - 'es-es': 'Retrocediendo', + 'ru-ru': 'Откат назад', + 'es-es': 'Retroceder', 'fr-fr': 'Retour en arrière', 'uk-ua': 'Відкат назад', - 'de-ch': 'Zurückrollen', + 'de-ch': 'Rückwärts', 'pt-br': 'Revertendo', 'hr-hr': 'Vraćanje unatrag', - nb: 'Rulle tilbake', + nb: 'Ruller tilbake', }, uploading: { 'en-us': 'Uploading', @@ -115,7 +115,7 @@ export const wbText = createDictionary({ 'fr-fr': 'Remplir vers le bas', 'uk-ua': 'Заповнити вниз', 'de-ch': 'Nach unten füllen', - 'pt-br': 'Preencher abaixo', + 'pt-br': 'Preencha até o final', 'hr-hr': 'Popuni prema dolje', nb: 'Fyll ned', }, @@ -167,20 +167,20 @@ export const wbText = createDictionary({ 'en-us': 'Live validation is not a complete substitute for regular validation. Make sure to perform regular validation before uploading.', 'de-ch': - 'Hinweis: Die Live-Validierung ist eine experimentelle Funktion und kein Ersatz für die reguläre Validierung.', + 'Die Live-Validierung ist kein vollständiger Ersatz für die reguläre Validierung. Führen Sie daher unbedingt vor dem Hochladen eine reguläre Validierung durch.', 'es-es': - 'Tenga en cuenta que la validación en vivo es una función experimental y no sustituye a la validación regular.', + 'La validación en tiempo real no sustituye por completo la validación periódica. Asegúrese de realizar la validación periódica antes de subir los archivos.', 'fr-fr': - 'Notez que la validation en direct est une fonctionnalité expérimentale et ne remplace pas la validation régulière.', + "La validation en direct ne remplace pas entièrement la validation régulière. Assurez-vous d'effectuer une validation régulière avant de mettre en ligne vos données.", 'ru-ru': - 'Обратите внимание, что проверка в реальном времени является экспериментальной функцией и не заменяет обычную проверку.', + 'Проверка в реальном времени не является полной заменой обычной проверки. Перед загрузкой обязательно проведите обычную проверку.', 'uk-ua': - 'Зауважте, що перевірка в реальному часі є експериментальною функцією і не замінює звичайну перевірку.', + 'Перевірка в реальному часі не є повною заміною регулярної перевірки. Обов’язково виконуйте регулярну перевірку перед завантаженням.', 'pt-br': - 'Observação: a validação em tempo real é um recurso experimental e não substitui a validação regular.', + 'A validação em tempo real não substitui completamente a validação regular. Certifique-se de realizar a validação regular antes de fazer o upload.', 'hr-hr': - 'Napomena: validacija uživo je eksperimentalna značajka i nije zamjena za redovnu validaciju.', - nb: 'Merk at live-validering er en eksperimentell funksjon og ikke en erstatning for vanlig validering.', + 'Validacija uživo nije potpuna zamjena za redovnu validaciju. Obavezno izvršite redovnu validaciju prije prijenosa.', + nb: 'Live-validering er ikke en fullstendig erstatning for vanlig validering. Sørg for å utføre regelmessig validering før opplasting.', }, changeOwner: { 'en-us': 'Change Owner', @@ -232,7 +232,7 @@ export const wbText = createDictionary({ 'es-es': 'Valor de sustitución', 'fr-fr': 'Valeur de remplacement', 'uk-ua': 'Значення для заміни', - 'de-ch': 'Ersatzwert', + 'de-ch': 'Wiederbeschaffungswert', 'pt-br': 'Valor de substituição', 'hr-hr': 'Zamjenska vrijednost', nb: 'Erstatningsverdi', @@ -365,7 +365,7 @@ export const wbText = createDictionary({ 'uk-ua': 'Функція «Застосувати все» недоступна, поки триває перевірка даних.', 'de-ch': - 'Die Option „Alle anwenden" ist während der Datenprüfung nicht verfügbar.', + 'Die Option „Alle anwenden“ ist während der Datenprüfung nicht verfügbar.', 'pt-br': 'A opção "Aplicar tudo" não está disponível enquanto a verificação de dados estiver em andamento.', 'hr-hr': '"Primijeni sve" nije dostupno dok je u tijeku Provjera podataka.', @@ -390,16 +390,16 @@ export const wbText = createDictionary({ 'es-es': 'La reversión eliminará los nuevos registros de datos que este conjunto de datos agregó a la base de datos Specify. La reversión completa se cancelará si alguno de los datos cargados ha sido referenciado (reutilizado) por otros registros de datos desde que se cargaron.', 'fr-fr': - "La restauration supprimera les nouveaux enregistrements de données que cet ensemble de données a ajoutés à la base de données Specify. L'intégralité de la restauration sera annulée si l'une des données téléchargées a été référencée (réutilisée) par d'autres enregistrements de données depuis leur téléchargement.", + "L'annulation supprimera les nouveaux enregistrements de données ajoutés par cet ensemble de données à la base de données Specify. L'annulation complète sera annulée si des données importées ont été référencées (réutilisées) par d'autres enregistrements de données depuis leur importation.", 'uk-ua': - 'Відкат видалить нові записи даних, додані цим набором даних до бази даних Specify. Повний відкат буде скасовано, якщо на будь-які завантажені дані посилалися (повторно використовували) інші записи даних після їх завантаження.', + 'Відкат видалить нові записи даних, які цей набір даних додав до бази даних Specify. Повний відкат буде скасовано, якщо будь-які завантажені дані були використані повторно (повторно використані) іншими записами даних з моменту їх завантаження.', 'de-ch': - 'Durch das Zurücksetzen werden die neuen Datensätze, die dieses Dataset der Specify-Datenbank hinzugefügt hat, entfernt. Der gesamte Rollback-Vorgang wird abgebrochen, falls hochgeladene Daten seit ihrem Hochladen von anderen Datensätzen referenziert (wiederverwendet) wurden.', + 'Durch das Zurücksetzen werden die neuen Datensätze, die dieser Datensatz der Specify-Datenbank hinzugefügt hat, entfernt. Der gesamte Rollback-Vorgang wird abgebrochen, falls hochgeladene Daten seit ihrem Hochladen von anderen Datensätzen referenziert (wiederverwendet) wurden.', 'pt-br': - 'A reversão removerá os novos registros de dados que este Conjunto de Dados adicionou ao banco de dados Specify. A reversão será totalmente cancelada se algum dos dados carregados tiver sido referenciado (reutilizado) por outros registros de dados desde o seu carregamento.', + 'A reversão removerá os novos registros de dados que este conjunto de dados adicionou ao banco de dados Specify. A reversão completa será cancelada se algum dos dados carregados tiver sido referenciado (reutilizado) por outros registros de dados desde o seu carregamento.', 'hr-hr': - 'Vraćanje će ukloniti nove podatkovne zapise koje je ovaj skup podataka dodao u bazu podataka Specify. Cijelo vraćanje bit će otkazano ako su bilo koji od prenesenih podataka referencirani (ponovno korišteni) od strane drugih podatkovnih zapisa od njihovog prijenosa.', - nb: 'Tilbakerulling vil fjerne de nye datapostene som dette datasettet la til i Specify-databasen. Hele tilbakerullingen vil bli avbrutt hvis noen av de opplastede dataene har blitt referert til (brukt på nytt) av andre dataposter siden de ble lastet opp.', + 'Vraćanje će ukloniti nove zapise podataka koje je ovaj skup podataka dodao u bazu podataka Specify. Cijelo vraćanje bit će otkazano ako su bilo koji od prenesenih podataka referencirani (ponovno korišteni) od strane drugih zapisa podataka od njihovog prijenosa.', + nb: 'Tilbakerulling vil fjerne de nye datapostene som dette datasettet la til i Specify-databasen. Hele tilbakerullingen vil bli avbrutt hvis noen av de opplastede dataene har blitt referert til (gjenbrukt) av andre dataposter siden de ble lastet opp.', }, startUpload: { 'en-us': 'Begin Data Set Upload?', @@ -415,19 +415,21 @@ export const wbText = createDictionary({ startUploadDescription: { 'en-us': 'Uploading this will add all new records in this data set to your database.', - 'ru-ru': 'Загрузка набора данных добавит данные в базу данных Specify.', + 'ru-ru': + 'Загрузка этих данных добавит все новые записи из данного набора данных в вашу базу данных.', 'es-es': - 'Cargar el conjunto de datos agregará los datos a la base de datos de especificación.', + 'Al subir este archivo, se añadirán todos los registros nuevos de este conjunto de datos a su base de datos.', 'fr-fr': - "Le téléchargement de l'ensemble de données ajoutera les données à la base de données Specify.", - 'uk-ua': 'Завантаження набору даних додасть дані до бази даних Specify.', + 'Le chargement de ce fichier ajoutera tous les nouveaux enregistrements de cet ensemble de données à votre base de données.', + 'uk-ua': + 'Завантаження цього додасть усі нові записи з цього набору даних до вашої бази даних.', 'de-ch': - 'Durch das Hochladen des Datensatzes werden die Daten der Specify-Datenbank hinzugefügt.', + 'Durch das Hochladen dieser Datei werden alle neuen Datensätze in Ihre Datenbank eingefügt.', 'pt-br': - 'O carregamento do conjunto de dados adicionará os dados ao banco de dados especificado.', + 'Ao fazer o upload, todos os novos registros deste conjunto de dados serão adicionados ao seu banco de dados.', 'hr-hr': - 'Prijenosom skupa podataka podaci će se dodati u bazu podataka Specify.', - nb: 'Opplasting av datasettet vil legge til dataene i Specify-databasen.', + 'Prijenosom ovoga dodat ćete sve nove zapise iz ovog skupa podataka u svoju bazu podataka.', + nb: 'Hvis du laster opp dette, legges alle nye poster i dette datasettet til i databasen din.', }, deleteDataSet: { 'en-us': 'Delete this data set?', @@ -446,23 +448,23 @@ export const wbText = createDictionary({ 'ru-ru': 'Удаление набора данных приводит к безвозвратному удалению его и его плана загрузки. План загрузки не будет доступным для повторного использования; откат больше не будет возможным для загруженного набора данных.', 'es-es': - 'Eliminar un conjunto de datos lo elimina de forma permanente junto con su plan de carga. Las asignaciones de datos ya no estarán disponibles para su reutilización con otros conjuntos de datos. Además, después de eliminar, la reversión ya no será una opción para un conjunto de datos cargado.', + 'Al eliminar un conjunto de datos, este se elimina permanentemente, junto con su plan de carga. Esta asignación ya no estará disponible para su reutilización con otros conjuntos de datos. Una vez eliminado, la opción de revertir los cambios ya no estará disponible.', 'fr-fr': - "La suppression d'un ensemble de données le supprime définitivement ainsi que son plan de téléchargement. Les mappages de données ne pourront plus être réutilisés avec d'autres ensembles de données. De plus, après la suppression, la restauration ne sera plus une option pour un ensemble de données téléchargé.", + "La suppression d'un jeu de données entraîne sa suppression définitive ainsi que celle de son plan de chargement. Ce mappage ne pourra plus être réutilisé avec d'autres jeux de données. Une fois supprimé, il ne sera plus possible de revenir en arrière.", 'uk-ua': - 'Видалення набору даних остаточно видаляє його та його план завантаження. Зіставлення даних більше не буде доступним для повторного використання з іншими наборами даних. Крім того, після видалення відкат більше не буде доступним для завантаженого набору даних.', + 'Видалення набору даних назавжди видаляє його та його план завантаження. Це зіставлення більше не буде доступним для повторного використання з іншими наборами даних. Після видалення відкат більше не буде можливим.', 'de-ch': - 'Durch das Löschen eines Datensatzes werden dieser und sein Upload-Plan endgültig entfernt. Datenzuordnungen können dann nicht mehr für andere Datensätze wiederverwendet werden. Nach dem Löschen ist außerdem keine Wiederherstellung des hochgeladenen Datensatzes mehr möglich.', + 'Durch das Löschen eines Datensatzes werden dieser und sein Upload-Plan endgültig entfernt. Diese Zuordnung kann dann nicht mehr für andere Datensätze wiederverwendet werden. Nach dem Löschen ist ein Zurücksetzen nicht mehr möglich.', 'pt-br': - 'A exclusão de um conjunto de dados remove permanentemente o conjunto e seu plano de upload. Os mapeamentos de dados não estarão mais disponíveis para reutilização com outros conjuntos de dados. Além disso, após a exclusão, a reversão não estará mais disponível para um conjunto de dados carregado.', + 'A exclusão de um conjunto de dados remove permanentemente o conjunto e seu plano de upload. Esse mapeamento não estará mais disponível para reutilização com outros conjuntos de dados. Uma vez excluído, o recurso de reversão não estará mais disponível.', 'hr-hr': - 'Trajnim brisanjem skupa podataka uklanja se on i njegov plan prijenosa. Mapiranje podataka više neće biti dostupno za ponovnu upotrebu s drugim skupovima podataka. Također, nakon brisanja, vraćanje na prethodno stanje više neće biti opcija za preneseni skup podataka.', - nb: 'Hvis du sletter et datasett permanent, fjernes det og opplastingsplanen. Datatilordninger vil ikke lenger være tilgjengelige for gjenbruk med andre datasett. Etter sletting vil heller ikke tilbakerulling lenger være et alternativ for et opplastet datasett.', + 'Trajnim brisanjem skupa podataka uklanja se on i njegov plan prijenosa. Ovo mapiranje više neće biti dostupno za ponovnu upotrebu s drugim skupovima podataka. Nakon brisanja, vraćanje na prethodno stanje više neće biti moguće.', + nb: 'Hvis du sletter et datasett permanent, fjernes det og opplastingsplanen. Denne kartleggingen vil ikke lenger være tilgjengelig for gjenbruk med andre datasett. Når den er slettet, vil det ikke lenger være mulig å rulle tilbake.', }, dataSetDeleted: { 'en-us': 'Data set successfully deleted', 'ru-ru': 'Набор данных успешно удален', - 'es-es': 'Conjunto de datos eliminado con éxito', + 'es-es': 'Conjunto de datos eliminado correctamente', 'fr-fr': 'Ensemble de données supprimé avec succès', 'uk-ua': 'Набір даних успішно видалено', 'de-ch': 'Datensatz erfolgreich gelöscht', @@ -472,14 +474,14 @@ export const wbText = createDictionary({ }, dataSetDeletedDescription: { 'en-us': 'This data set was deleted.', - 'ru-ru': 'Набор данных успешно удален.', - 'es-es': 'Conjunto de datos eliminado con éxito.', - 'fr-fr': 'Ensemble de données supprimé avec succès.', - 'uk-ua': 'Набір даних успішно видалено.', - 'de-ch': 'Datensatz erfolgreich gelöscht.', - 'pt-br': 'Conjunto de dados excluído com sucesso.', - 'hr-hr': 'Skup podataka uspješno izbrisan.', - nb: 'Datasettet er slettet.', + 'ru-ru': 'Этот набор данных был удален.', + 'es-es': 'Este conjunto de datos fue eliminado.', + 'fr-fr': 'Ces données ont été supprimées.', + 'uk-ua': 'Цей набір даних було видалено.', + 'de-ch': 'Dieser Datensatz wurde gelöscht.', + 'pt-br': 'Este conjunto de dados foi excluído.', + 'hr-hr': 'Ovaj skup podataka je izbrisan.', + nb: 'Dette datasettet ble slettet.', }, revertChanges: { 'en-us': 'Revert Unsaved Changes?', @@ -498,18 +500,18 @@ export const wbText = createDictionary({ 'ru-ru': 'Это действие приведет к отмене всех изменений, внесенных в набор данных с момента последнего сохранения.', 'es-es': - 'Esta acción descartará todos los cambios realizados en el conjunto de datos desde la última vez que se guardó.', + 'Esta acción descartará todos los cambios realizados en el conjunto de datos desde el último guardado.', 'fr-fr': - "Cette action annulera toutes les modifications apportées à l'ensemble de données depuis le dernier enregistrement.", + "Cette action annulera toutes les modifications apportées à l'ensemble de données depuis la dernière sauvegarde.", 'uk-ua': - 'Ця дія призведе до скасування всіх змін, внесених до набору даних після останнього збереження.', + 'Ця дія скасує всі зміни, внесені до набору даних з моменту останнього збереження.', 'de-ch': - 'Durch diese Aktion werden alle Änderungen verworfen, die seit dem letzten Speichern am Datensatz vorgenommen wurden.', + 'Durch diese Aktion werden alle Änderungen verworfen, die seit dem letzten Speichern an dem Datensatz vorgenommen wurden.', 'pt-br': 'Esta ação descartará todas as alterações feitas no conjunto de dados desde a última vez que foi salvo.', 'hr-hr': 'Ova radnja će odbaciti sve promjene napravljene u skupu podataka od posljednjeg spremanja.', - nb: 'Denne handlingen vil forkaste alle endringer som er gjort i datasettet siden forrige lagring.', + nb: 'Denne handlingen vil forkaste alle endringer som er gjort i datasettet siden siste lagring.', }, saving: { 'en-us': 'Saving...', @@ -525,11 +527,12 @@ export const wbText = createDictionary({ wbUnloadProtect: { 'en-us': 'Changes to this data set have not been saved.', 'ru-ru': 'Изменения в этом наборе данных не были сохранены.', - 'es-es': 'Los cambios a este conjunto de datos no se han guardado.', + 'es-es': + 'Los cambios realizados en este conjunto de datos no se han guardado.', 'fr-fr': "Les modifications apportées à cet ensemble de données n'ont pas été enregistrées.", - 'uk-ua': 'Зміни в цьому наборі даних не збережено.', - 'de-ch': 'Die Änderungen an diesem Datensatz wurden nicht gespeichert.', + 'uk-ua': 'Зміни до цього набору даних не збережено.', + 'de-ch': 'Änderungen an diesem Datensatz wurden nicht gespeichert.', 'pt-br': 'As alterações feitas neste conjunto de dados não foram salvas.', 'hr-hr': 'Promjene ovog skupa podataka nisu spremljene.', nb: 'Endringer i dette datasettet er ikke lagret.', @@ -543,7 +546,7 @@ export const wbText = createDictionary({ 'Aucun enregistrement correspondant pour la table à correspondance obligatoire.', 'uk-ua': 'Немає відповідного запису для таблиці обовʼязкової відповідності.', - 'de-ch': 'Kein passender Datensatz für die Tabelle „muss übereinstimmen".', + 'de-ch': 'Kein passender Datensatz für die Tabelle „muss übereinstimmen“.', 'pt-br': 'Não foi encontrado nenhum registro correspondente na tabela de correspondência obrigatória.', 'hr-hr': 'Nema odgovarajućeg zapisa za tablicu s obaveznim podudaranjem.', @@ -570,12 +573,12 @@ export const wbText = createDictionary({ }, validationNoErrors: { 'en-us': 'Validation Completed with No Errors', - 'ru-ru': 'Проверка завершена без ошибок', + 'ru-ru': 'Проверка завершена без ошибок.', 'es-es': 'Validación completada sin errores', 'fr-fr': 'Validation terminée sans erreur', - 'uk-ua': 'Перевірка завершена без помилок', - 'de-ch': 'Validierung erfolgreich abgeschlossen (keine Fehler)', - 'pt-br': 'Validação concluída sem erros', + 'uk-ua': 'Перевірку завершено без помилок', + 'de-ch': 'Validierung erfolgreich abgeschlossen.', + 'pt-br': 'Validação concluída sem erros.', 'hr-hr': 'Validacija završena bez grešaka', nb: 'Validering fullført uten feil', }, @@ -583,20 +586,20 @@ export const wbText = createDictionary({ 'en-us': 'The validation process found no errors. Your data set is ready to be imported.', 'ru-ru': - 'Проверка завершена без ошибок. Этот набора данных готов к загрузке в базу данных.', + 'В процессе проверки ошибок не обнаружено. Ваш набор данных готов к импорту.', 'es-es': - 'La validación no encontró errores, está listo para ser cargado en la base de datos.', + 'El proceso de validación no detectó errores. Su conjunto de datos está listo para ser importado.', 'fr-fr': - "La validation n'a trouvé aucune erreur, elle est prête à être téléchargée dans la base de données.", + "Le processus de validation n'a détecté aucune erreur. Vos données sont prêtes à être importées.", 'uk-ua': - 'Перевірка не виявила помилок, вона готова до завантаження в базу даних.', + 'Процес перевірки не виявив помилок. Ваш набір даних готовий до імпорту.', 'de-ch': - 'Bei der Validierung wurden keine Fehler festgestellt, die Datei kann nun in die Datenbank hochgeladen werden.', + 'Der Validierungsprozess hat keine Fehler gefunden. Ihr Datensatz kann nun importiert werden.', 'pt-br': - 'A validação não encontrou erros, o arquivo está pronto para ser carregado no banco de dados.', + 'O processo de validação não encontrou erros. Seu conjunto de dados está pronto para ser importado.', 'hr-hr': - 'Validacija nije pronašla greške, spremno je za učitavanje u bazu podataka.', - nb: 'Valideringen fant ingen feil, den er klar til å lastes opp til databasen.', + 'Postupak validacije nije pronašao pogreške. Vaš skup podataka spreman je za uvoz.', + nb: 'Valideringsprosessen fant ingen feil. Datasettet ditt er klart til importering.', }, validationReEditWarning: { 'en-us': @@ -604,27 +607,27 @@ export const wbText = createDictionary({ 'ru-ru': 'Примечание: Если этот набор данных отредактирован и повторно сохранен, необходимо заново запустить проверку перед загрузкой, чтобы убедиться, что ошибки не были внесены.', 'es-es': - 'Nota: si este conjunto de datos se edita y se vuelve a guardar, se debe volver a ejecutar Validar antes de cargar para verificar que no se hayan introducido errores.', + 'Si edita este conjunto de datos, asegúrese de validarlo antes de subirlo.', 'fr-fr': - "Remarque : Si cet ensemble de données est modifié et réenregistré, la validation doit être réexécutée avant le téléchargement pour vérifier qu'aucune erreur n'a été introduite.", + 'Si ces données sont modifiées, assurez-vous de les valider avant de les télécharger.', 'uk-ua': - 'Примітка. Якщо цей набір даних відредаговано та повторно збережено, перед завантаженням слід повторно запустити перевірку, щоб переконатися, що не було допущено помилок.', + 'Якщо цей набір даних редагується, обов’язково перевірте його перед завантаженням.', 'de-ch': - 'Hinweis: Wenn dieser Datensatz bearbeitet und erneut gespeichert wird, sollte die Validierung vor dem Hochladen erneut ausgeführt werden, um sicherzustellen, dass keine Fehler entstanden sind.', + 'Wenn dieser Datensatz bearbeitet wird, muss er vor dem Hochladen validiert werden.', 'pt-br': - 'Observação: Se este conjunto de dados for editado e salvo novamente, a validação deverá ser executada novamente antes do upload para verificar se não foram introduzidos erros.', + 'Caso este conjunto de dados seja editado, certifique-se de validá-lo antes de fazer o upload.', 'hr-hr': - 'Napomena: Ako se ovaj skup podataka uredi i ponovno spremi, prije prijenosa treba ponovno pokrenuti Validaciju kako bi se provjerilo da nisu unesene nikakve pogreške.', - nb: 'Merk: Hvis dette datasettet redigeres og lagres på nytt, bør Validate kjøres på nytt før opplasting for å bekrefte at det ikke har oppstått noen feil.', + 'Ako se ovaj skup podataka uređuje, obavezno ga provjerite prije prijenosa.', + nb: 'Hvis dette datasettet redigeres, må du validere det før opplasting.', }, validationErrors: { 'en-us': 'Validation Completed with Errors', - 'ru-ru': 'Проверка завершена с ошибками', + 'ru-ru': 'Проверка завершена с ошибками.', 'es-es': 'Validación completada con errores', 'fr-fr': 'Validation terminée avec des erreurs', - 'uk-ua': 'Перевірка виконана з помилками', + 'uk-ua': 'Перевірку завершено з помилками', 'de-ch': 'Validierung mit Fehlern abgeschlossen', - 'pt-br': 'Validação concluída com erros', + 'pt-br': 'Validação concluída com erros.', 'hr-hr': 'Validacija završena s pogreškama', nb: 'Validering fullført med feil', }, @@ -646,9 +649,9 @@ export const wbText = createDictionary({ 'fr-fr': 'Téléchargement terminé avec succès', 'uk-ua': 'Завантаження успішно завершено', 'de-ch': 'Upload erfolgreich abgeschlossen', - 'pt-br': 'Envio concluído sem erros.', - 'hr-hr': 'Prijenos završen bez grešaka', - nb: 'Opplasting fullført uten feil', + 'pt-br': 'Envio concluído com sucesso.', + 'hr-hr': 'Prijenos uspješno završen', + nb: 'Opplasting fullført', }, uploadSuccessfulDescription: { 'en-us': @@ -662,19 +665,19 @@ export const wbText = createDictionary({ 'uk-ua': 'Ваші дані завантажено в базу даних. Щоб переглянути кількість нових записів, доданих до кожної таблиці, натисніть «Результати» на панелі інструментів над сіткою даних.', 'de-ch': - 'Ihre Daten wurden in die Datenbank hochgeladen. Um die Anzahl der neu hinzugefügten Datensätze in jeder Tabelle anzuzeigen, klicken Sie in der Symbolleiste über dem Datenraster auf „Ergebnisse".', + 'Ihre Daten wurden erfolgreich in die Datenbank hochgeladen. Die Anzahl der neu hinzugefügten Datensätze in den einzelnen Tabellen wird unten angezeigt.', 'pt-br': - 'Seus dados foram enviados para o banco de dados. Para ver o número de novos registros adicionados a cada tabela, clique em "Resultados" na barra de ferramentas acima da grade de dados.', + 'Seus dados foram carregados com sucesso no banco de dados. Você pode ver abaixo o número de novos registros adicionados a cada tabela.', 'hr-hr': - 'Kliknite gumb "Rezultati" da biste vidjeli broj novih zapisa dodanih svakoj tablici baze podataka.', - nb: 'Klikk på «Resultater»-knappen for å se antall nye poster som er lagt til i hver databasetabell.', + 'Vaši su podaci uspješno preneseni u bazu podataka. Broj novih zapisa dodanih svakoj tablici možete vidjeti u nastavku.', + nb: 'Dataene dine er lastet opp til databasen. Du kan se antall nye poster som er lagt til i hver tabell nedenfor.', }, uploadErrors: { 'en-us': 'Upload Failed Due to Error Cells', - 'ru-ru': 'Ошибка загрузки из-за ошибок', - 'es-es': 'Carga fallida debido a celdas de error', - 'fr-fr': "Échec du téléchargement en raison de cellules d'erreur", - 'uk-ua': 'Помилка завантаження через клітинки помилок', + 'ru-ru': 'Загрузка не удалась из-за ошибок в ячейках.', + 'es-es': 'Error al cargar debido a celdas con errores.', + 'fr-fr': 'Échec du chargement en raison de cellules erronées', + 'uk-ua': 'Завантаження не вдалося через помилки в клітинках', 'de-ch': 'Upload aufgrund fehlerhafter Zellen fehlgeschlagen', 'pt-br': 'O carregamento falhou devido a células com erro.', 'hr-hr': 'Prijenos nije uspio zbog ćelija s pogreškom', @@ -703,15 +706,15 @@ export const wbText = createDictionary({ 'de-ch': 'Überprüfen Sie den Datensatz und die Mauszeigerhinweise für jede Fehlerzelle. Nehmen Sie anschließend die entsprechenden Korrekturen vor. Speichern Sie die Daten und versuchen Sie es erneut mit {type:string}.', 'es-es': - 'Valide el conjunto de datos y revise las sugerencias que aparecen al pasar el ratón por encima de cada celda con errores, luego realice las correcciones pertinentes. Guarde y vuelva a intentarlo {type:string}.', + 'Valide el conjunto de datos y revise las sugerencias que aparecen al pasar el ratón por encima de cada celda con errores, luego realice las correcciones pertinentes. Guarde y vuelva a intentarlo con {type:string}.', 'fr-fr': "Validez l'ensemble de données et consultez les infobulles pour chaque cellule d'erreur, puis effectuez les corrections nécessaires. Enregistrez et réessayez {type:string}.", 'pt-br': - 'Valide o conjunto de dados e revise as dicas ao passar o mouse para cada célula com erro e, em seguida, faça as correções apropriadas. Salve e tente novamente o {type:string}.', + 'Valide o conjunto de dados e revise as dicas ao passar o mouse para cada célula de erro e, em seguida, faça as correções apropriadas. Salve e tente novamente o {type:string}.', 'ru-ru': 'Проверьте набор данных и просмотрите подсказки при наведении курсора мыши на каждую ячейку с ошибкой, затем внесите соответствующие исправления. Сохраните и повторите попытку {type:string}.', 'uk-ua': - 'Перевірте набір даних і перегляньте підказки під час наведення курсора миші для кожної клітинки з помилкою, потім внесіть відповідні виправлення. Збережіть і повторіть спробу {type:string}.', + 'Перевірте набір даних і перегляньте підказки при наведенні курсора миші для кожної клітинки з помилкою, потім внесіть відповідні виправлення. Збережіть дані та повторіть спробу {type:string}.', 'hr-hr': 'Provjerite valjanost skupa podataka i pregledajte upute za prelazak mišem preko svake ćelije s pogreškom, a zatim izvršite odgovarajuće ispravke. Spremite i ponovno pokušajte {type:string}.', nb: 'Valider datasettet og se gjennom musepekertipsene for hver feilcelle, og gjør deretter de nødvendige rettelsene. Lagre og prøv {type:string} på nytt.', @@ -719,9 +722,9 @@ export const wbText = createDictionary({ dataSetRollback: { 'en-us': 'Data set was rolled back successfully', 'ru-ru': 'Набор данных был успешно откачен', - 'es-es': 'El conjunto de datos se revirtió con éxito', - 'fr-fr': "L'ensemble de données a été restauré avec succès", - 'uk-ua': 'Набір даних успішно повернуто', + 'es-es': 'El conjunto de datos se revirtió correctamente.', + 'fr-fr': 'Les données ont été restaurées avec succès.', + 'uk-ua': 'Набір даних успішно відкочено', 'de-ch': 'Der Datensatz wurde erfolgreich zurückgesetzt.', 'pt-br': 'O conjunto de dados foi revertido com sucesso.', 'hr-hr': 'Skup podataka uspješno je vraćen u prethodno stanje', @@ -733,18 +736,18 @@ export const wbText = createDictionary({ 'ru-ru': 'Этот откаченный набор данных сохранен и может быть отредактирован или повторно загружен.', 'es-es': - 'Este conjunto de datos revertidos se guarda y se puede editar o volver a cargar.', + 'Este conjunto de datos se ha restaurado correctamente y se puede editar o volver a cargar.', 'fr-fr': - 'Cet ensemble de données restaurées est enregistré et peut être modifié ou téléchargé à nouveau.', + 'Ces données ont été restaurées avec succès et peuvent être modifiées ou téléchargées à nouveau.', 'uk-ua': - 'Цей відкочений набір даних зберігається та може бути відредагований або повторно завантажений.', + 'Цей набір даних успішно відкочено, і його можна редагувати або повторно завантажити.', 'de-ch': - 'Dieser zurückgesetzte Datensatz ist gespeichert und kann bearbeitet oder erneut hochgeladen werden.', + 'Dieser Datensatz wurde erfolgreich zurückgesetzt und kann bearbeitet oder erneut hochgeladen werden.', 'pt-br': - 'Este conjunto de dados revertido foi salvo e pode ser editado ou reenviado.', + 'Este conjunto de dados foi revertido com sucesso e pode ser editado ou reenviado.', 'hr-hr': - 'Ovaj vraćeni skup podataka je spremljen i može se uređivati ili ponovno prenijeti.', - nb: 'Dette tilbakerullede datasettet er lagret og kan redigeres eller lastes opp på nytt.', + 'Ovaj skup podataka uspješno je vraćen unatrag i može se urediti ili ponovno prenijeti.', + nb: 'Dette datasettet er rullet tilbake, og kan redigeres eller lastes opp på nytt.', }, validationCanceled: { 'en-us': 'Validation Cancelled', @@ -760,14 +763,21 @@ export const wbText = createDictionary({ validationCanceledDescription: { 'en-us': 'The validation operation was cancelled. Please try again before uploading.', - 'ru-ru': 'Проверка набора данных отменена.', - 'es-es': 'Se canceló la validación del conjunto de datos.', - 'fr-fr': "Validation de l'ensemble de données annulée.", - 'uk-ua': 'Перевірку набору даних скасовано.', - 'de-ch': 'Die Validierung des Datensatzes wurde abgebrochen.', - 'pt-br': 'Validação do conjunto de dados cancelada.', - 'hr-hr': 'Validacija skupa podataka otkazana.', - nb: 'Validering av datasett avbrutt.', + 'ru-ru': + 'Операция проверки была отменена. Пожалуйста, попробуйте еще раз перед загрузкой.', + 'es-es': + 'La operación de validación se ha cancelado. Inténtelo de nuevo antes de subir el archivo.', + 'fr-fr': + "L'opération de validation a été annulée. Veuillez réessayer avant de télécharger.", + 'uk-ua': + 'Операцію перевірки скасовано. Будь ласка, спробуйте ще раз перед завантаженням.', + 'de-ch': + 'Der Validierungsvorgang wurde abgebrochen. Bitte versuchen Sie es erneut, bevor Sie den Upload starten.', + 'pt-br': + 'A operação de validação foi cancelada. Tente novamente antes de enviar.', + 'hr-hr': + 'Postupak validacije je otkazan. Pokušajte ponovno prije prijenosa.', + nb: 'Valideringsoperasjonen ble avbrutt. Prøv på nytt før du laster opp.', }, rollbackCanceled: { 'en-us': 'Rollback Cancelled', @@ -783,14 +793,20 @@ export const wbText = createDictionary({ rollbackCanceledDescription: { 'en-us': 'The rollback was cancelled. No changes were made to the database.', - 'ru-ru': 'Откат набора данных отменен.', - 'es-es': 'Reversión del conjunto de datos cancelada.', - 'fr-fr': 'Restauration de l’ensemble de données annulée.', - 'uk-ua': 'Відкат набору даних скасовано.', - 'de-ch': 'Datensatz-Rollback abgebrochen.', - 'pt-br': 'Reversão do conjunto de dados cancelada.', - 'hr-hr': 'Vraćanje skupa podataka otkazano.', - nb: 'Tilbakerulling av datasett avbrutt.', + 'ru-ru': + 'Откат был отменен. Никаких изменений в базу данных внесено не было.', + 'es-es': + 'La reversión fue cancelada. No se realizaron cambios en la base de datos.', + 'fr-fr': + "La restauration a été annulée. Aucune modification n'a été apportée à la base de données.", + 'uk-ua': 'Відкат скасовано. До бази даних не внесено жодних змін.', + 'de-ch': + 'Der Rollback wurde abgebrochen. Es wurden keine Änderungen an der Datenbank vorgenommen.', + 'pt-br': + 'O rollback foi cancelado. Nenhuma alteração foi feita no banco de dados.', + 'hr-hr': + 'Vraćanje na prethodnu verziju je otkazano. U bazi podataka nisu napravljene nikakve promjene.', + nb: 'Tilbakerullingen ble avbrutt. Ingen endringer ble gjort i databasen.', }, uploadCanceled: { 'en-us': 'Upload Cancelled', @@ -805,14 +821,20 @@ export const wbText = createDictionary({ }, uploadCanceledDescription: { 'en-us': 'The upload was cancelled. No changes were made to the database.', - 'ru-ru': 'Загрузка набора данных отменена.', - 'es-es': 'Carga de conjunto de datos cancelada.', - 'fr-fr': "Téléchargement de l'ensemble de données annulé.", - 'uk-ua': 'Завантаження набору даних скасовано.', - 'de-ch': 'Der Upload des Datensatzes wurde abgebrochen.', - 'pt-br': 'Envio do conjunto de dados cancelado.', - 'hr-hr': 'Prijenos skupa podataka otkazan.', - nb: 'Opplasting av datasett avbrutt.', + 'ru-ru': + 'Загрузка была отменена. Никаких изменений в базу данных внесено не было.', + 'es-es': + 'La carga fue cancelada. No se realizaron cambios en la base de datos.', + 'fr-fr': + "Le chargement a été annulé. Aucune modification n'a été apportée à la base de données.", + 'uk-ua': 'Завантаження скасовано. До бази даних не внесено жодних змін.', + 'de-ch': + 'Der Upload wurde abgebrochen. Es wurden keine Änderungen an der Datenbank vorgenommen.', + 'pt-br': + 'O carregamento foi cancelado. Nenhuma alteração foi feita no banco de dados.', + 'hr-hr': + 'Prijenos je otkazan. U bazi podataka nisu napravljene nikakve promjene.', + nb: 'Opplastingen ble avbrutt. Ingen endringer ble gjort i databasen.', }, coordinateConverter: { 'en-us': 'Geocoordinate Format', @@ -846,8 +868,8 @@ export const wbText = createDictionary({ 'es-es': '(cadena vacía)', 'fr-fr': '(chaîne vide)', 'uk-ua': '(порожній рядок)', - 'de-ch': '(leere Zeichenkette)', - 'pt-br': '(string vazia)', + 'de-ch': '(leerer String)', + 'pt-br': '(cadeia vazia)', 'hr-hr': '(prazan niz)', nb: '(tom streng)', }, @@ -866,20 +888,20 @@ export const wbText = createDictionary({ 'en-us': 'A mapping needs to be defined before this data set can be validated', 'ru-ru': - 'План загрузки должен быть определен до того, как этот набор данных может быть проверен', + 'Перед проверкой этого набора данных необходимо определить соответствие между данными и данными.', 'es-es': - 'Se necesita definir un Plan de Carga antes de poder Validar este Conjunto de Datos', + 'Es necesario definir una correspondencia antes de poder validar este conjunto de datos.', 'fr-fr': - 'Un plan de téléchargement doit être défini avant que cet ensemble de données puisse être validé', + 'Il est nécessaire de définir une correspondance avant de pouvoir valider cet ensemble de données.', 'uk-ua': - 'Перед перевіркою цього набору даних необхідно визначити план завантаження', + 'Перш ніж цей набір даних можна буде перевірити, необхідно визначити зіставлення.', 'de-ch': - 'Bevor dieser Datensatz validiert werden kann, muss ein Upload-Plan definiert werden.', + 'Bevor dieser Datensatz validiert werden kann, muss eine Zuordnung definiert werden.', 'pt-br': - 'É necessário definir um plano de upload antes que este conjunto de dados possa ser validado.', + 'É necessário definir um mapeamento antes que este conjunto de dados possa ser validado.', 'hr-hr': - 'Plan prijenosa potrebno je definirati prije nego što se ovaj skup podataka može validirati', - nb: 'En opplastingsplan må defineres før dette datasettet kan valideres', + 'Mapiranje je potrebno definirati prije nego što se ovaj skup podataka može validirati', + nb: 'En kartlegging må defineres før dette datasettet kan valideres', }, unavailableWhileEditing: { 'en-us': 'This action requires all changes to be saved', @@ -955,10 +977,10 @@ export const wbText = createDictionary({ }, unavailableWhenUploaded: { 'en-us': 'This tool does not work with uploaded data sets', - 'ru-ru': 'Этот инструмент не работает с загруженными наборами данных', - 'es-es': 'Esta herramienta no funciona con Conjuntos de Datos cargados', + 'ru-ru': 'Этот инструмент не работает с загруженными наборами данных.', + 'es-es': 'Esta herramienta no funciona con conjuntos de datos cargados.', 'fr-fr': - 'Cet outil ne fonctionne pas avec les ensembles de données téléchargés', + 'Cet outil ne fonctionne pas avec les ensembles de données téléchargés.', 'uk-ua': 'Цей інструмент не працює із завантаженими наборами даних', 'de-ch': 'Dieses Tool funktioniert nicht mit hochgeladenen Datensätzen.', 'pt-br': 'Esta ferramenta não funciona com conjuntos de dados carregados.', @@ -967,14 +989,14 @@ export const wbText = createDictionary({ }, dataSetDeletedOrNotFound: { 'en-us': 'This data set was deleted by another session.', - 'ru-ru': 'Набор данных был удален другим сеансом.', + 'ru-ru': 'Этот набор данных был удален в результате другой сессии.', 'es-es': 'Otra sesión ha eliminado el conjunto de datos.', - 'fr-fr': "L'ensemble de données a été supprimé par une autre session.", - 'uk-ua': 'Набір даних видалено іншим сеансом.', - 'de-ch': 'Der Datensatz wurde von einer anderen Sitzung gelöscht.', - 'pt-br': 'O conjunto de dados foi excluído por outra sessão.', - 'hr-hr': 'Skup podataka je izbrisan u drugoj sesiji.', - nb: 'Datasettet ble slettet av en annen økt.', + 'fr-fr': 'Ces données ont été supprimées par une autre session.', + 'uk-ua': 'Цей набір даних було видалено іншим сеансом.', + 'de-ch': 'Dieser Datensatz wurde von einer anderen Sitzung gelöscht.', + 'pt-br': 'Este conjunto de dados foi excluído por outra sessão.', + 'hr-hr': 'Ovaj skup podataka izbrisan je drugom sesijom.', + nb: 'Dette datasettet ble slettet av en annen økt.', }, includeDmsSymbols: { 'en-us': 'Include DMS Symbols', @@ -1036,7 +1058,7 @@ export const wbText = createDictionary({ 'en-us': 'Column first', 'ru-ru': 'Сначала столбцы', 'es-es': 'Columna primero', - 'de-ch': 'Spalte zuerst', + 'de-ch': 'erste Spalte', 'fr-fr': 'Colonne en premier', 'uk-ua': 'Спочатку стовпці', 'pt-br': 'Coluna primeiro', @@ -1071,8 +1093,8 @@ export const wbText = createDictionary({ 'es-es': 'Encontrar solo celdas completas', 'fr-fr': 'Rechercher uniquement des cellules entières', 'uk-ua': 'Знайти лише цілі клітини', - 'de-ch': 'Nur ganze Zellen finden', - 'pt-br': 'Encontrar apenas células inteiras', + 'de-ch': 'Finde nur ganze Zellen', + 'pt-br': 'Encontre apenas células inteiras', 'hr-hr': 'Pronađi samo cijele ćelije', nb: 'Finn bare hele celler', }, @@ -1094,7 +1116,7 @@ export const wbText = createDictionary({ 'fr-fr': 'Utiliser une expression régulière', 'uk-ua': 'Використовувати регулярний вираз', 'de-ch': 'Regulären Ausdruck verwenden', - 'pt-br': 'Usar expressão regular', + 'pt-br': 'Use expressões regulares', 'hr-hr': 'Koristite regularni izraz', nb: 'Bruk regulært uttrykk', }, @@ -1115,7 +1137,7 @@ export const wbText = createDictionary({ 'es-es': 'Opciones de reemplazo', 'fr-fr': 'Options de remplacement', 'uk-ua': 'Параметри заміни', - 'de-ch': 'Ersetzungsoptionen', + 'de-ch': 'Ersetzen Sie die Optionen', 'pt-br': 'Opções de substituição', 'hr-hr': 'Zamijeni opcije', nb: 'Erstatt alternativer', @@ -1148,7 +1170,7 @@ export const wbText = createDictionary({ 'es-es': 'Reemplazar siguiente ocurrencia', 'fr-fr': "Remplacer l'occurrence suivante", 'uk-ua': 'Замінити наступне входження', - 'de-ch': 'Nächstes Vorkommen ersetzen', + 'de-ch': 'Ersetze das nächste Vorkommen', 'pt-br': 'Substituir próxima ocorrência', 'hr-hr': 'Zamijeni sljedeće pojavljivanje', nb: 'Erstatt neste forekomst', @@ -1159,8 +1181,8 @@ export const wbText = createDictionary({ 'es-es': 'Importar conjunto de datos', 'fr-fr': 'Importer un ensemble de données', 'uk-ua': 'Імпорт набору даних', - 'de-ch': 'Datensatz importieren', - 'pt-br': 'Importar conjunto de dados', + 'de-ch': 'Importdatensatz', + 'pt-br': 'Conjunto de dados de importação', 'hr-hr': 'Uvoz skupa podataka', nb: 'Importer datasett', }, @@ -1182,8 +1204,8 @@ export const wbText = createDictionary({ 'es-es': 'Vista previa de Conjunto de Datos', 'fr-fr': "Aperçu de l'ensemble de données", 'uk-ua': 'Попередній перегляд набору даних', - 'de-ch': 'Datensatz-Vorschau', - 'pt-br': 'Pré-visualizar conjunto de dados', + 'de-ch': 'Vorschau-Datensatz', + 'pt-br': 'Conjunto de dados de pré-visualização', 'hr-hr': 'Pregled skupa podataka', nb: 'Forhåndsvisning av datasett', }, @@ -1273,7 +1295,7 @@ export const wbText = createDictionary({ 'es-es': 'Espacio', 'fr-fr': 'Espace', 'uk-ua': 'Пробіл', - 'de-ch': 'Leerzeichen', + 'de-ch': 'Raum', 'pt-br': 'Espaço', 'hr-hr': 'Prostor', nb: 'Rom', @@ -1328,8 +1350,8 @@ export const wbText = createDictionary({ 'es-es': 'Importar archivo', 'fr-fr': 'Importer le fichier', 'uk-ua': 'Імпорт файлу', - 'de-ch': 'Datei importieren', - 'pt-br': 'Importar arquivo', + 'de-ch': 'Importdatei', + 'pt-br': 'Arquivo de importação', 'hr-hr': 'Uvoz datoteke', nb: 'Importer fil', }, @@ -1368,14 +1390,14 @@ export const wbText = createDictionary({ }, wbsDialogEmpty: { 'en-us': 'Currently no data sets exist.', - 'ru-ru': 'В настоящее время наборов данных не существует.', + 'ru-ru': 'В настоящее время наборы данных отсутствуют.', 'es-es': 'Actualmente no existen conjuntos de datos.', - 'fr-fr': "Actuellement, aucun ensemble de données n'existe.", - 'uk-ua': 'Наразі не існує наборів даних.', + 'fr-fr': "Il n'existe actuellement aucun ensemble de données.", + 'uk-ua': 'Наразі наборів даних не існує.', 'de-ch': 'Derzeit existieren keine Datensätze.', 'pt-br': 'Atualmente não existem conjuntos de dados.', 'hr-hr': 'Trenutno ne postoje skupovi podataka.', - nb: 'Det finnes ingen datasett for øyeblikket.', + nb: 'For øyeblikket finnes det ingen datasett.', }, createDataSetInstructions: { 'en-us': 'Use "Import a file" or "Create New" to make a new one.', @@ -1567,7 +1589,7 @@ export const wbText = createDictionary({ 'fr-fr': "Statut de validation de l'ensemble de données", 'uk-ua': 'Статус перевірки набору даних', 'de-ch': 'Validierungsstatus des Datensatzes', - 'pt-br': 'Status de validação do conjunto de dados', + 'pt-br': 'Status de Validação do Conjunto de Dados', 'hr-hr': 'Status validacije skupa podataka', nb: 'Status for validering av datasett', }, @@ -1577,29 +1599,29 @@ export const wbText = createDictionary({ 'es-es': 'Cancelando...', 'fr-fr': 'Annulation...', 'uk-ua': 'Скасування...', - 'de-ch': 'Abbruch...', - 'pt-br': 'Abortando...', - 'hr-hr': 'Prekid...', + 'de-ch': 'Wird abgebrochen...', + 'pt-br': 'Cancelando...', + 'hr-hr': 'Otkazivanje...', nb: 'Avbryter...', }, wbStatusAbortFailed: { 'en-us': 'Failed cancelling the {operationName:string} operation. Please try again later', 'ru-ru': - 'Не удалось прервать операцию {operationName:string}. Пожалуйста, попробуйте позже', + 'Не удалось отменить операцию {operationName:string}. Пожалуйста, попробуйте позже.', 'es-es': - 'No se pudo abortar {operationName:string}. Por favor inténtelo más tarde', + 'No se pudo cancelar la operación {operationName:string}. Inténtelo de nuevo más tarde.', 'fr-fr': - "Échec de l'abandon de {operationName:string}. Veuillez réessayer plus tard", + "L'annulation de l'opération {operationName:string} a échoué. Veuillez réessayer ultérieurement.", 'uk-ua': 'Не вдалося перервати {operationName:string}. Будь ласка, спробуйте пізніше', 'de-ch': - 'Abbruch fehlgeschlagen {operationName:string}. Bitte versuchen Sie es später erneut.', + 'Die Operation {operationName:string} konnte nicht abgebrochen werden. Bitte versuchen Sie es später erneut.', 'pt-br': - 'Falha ao abortar {operationName:string}. Tente novamente mais tarde.', + 'Falha ao cancelar a operação {operationName:string}. Tente novamente mais tarde.', 'hr-hr': - 'Prekid nije uspio {operationName:string}. Pokušajte ponovno kasnije.', - nb: 'Avbryting mislyktes {operationName:string}. Prøv igjen senere.', + 'Nije uspjelo otkazivanje operacije {operationName:string}. Pokušajte ponovno kasnije.', + nb: 'Kunne ikke avbryte {operationName:string}-operasjonen. Prøv på nytt senere.', }, wbStatusOperationNoProgress: { comment: 'E.x, Validating...', @@ -1636,20 +1658,20 @@ export const wbText = createDictionary({ wbStatusPendingDescription: { 'en-us': '{operationName:string} of this data set should begin shortly.', 'ru-ru': - '{operationName:string} этого набора данных должно начаться в ближайшее время.', + '{operationName:string} этого набора данных должен начаться в ближайшее время.', 'es-es': - '{operationName:string} de este Conjunto de Datos debería comenzar en breve.', + '{operationName:string} de este conjunto de datos debería comenzar en breve.', 'fr-fr': '{operationName:string} de cet ensemble de données devrait commencer sous peu.', 'uk-ua': - '{operationName:string} цього набору даних має початися незабаром.', + '{operationName:string} цього набору даних має розпочатися найближчим часом.', 'de-ch': '{operationName:string} dieses Datensatzes sollte in Kürze beginnen.', 'pt-br': '{operationName:string} deste conjunto de dados deve começar em breve.', 'hr-hr': '{operationName:string} ovog skupa podataka trebalo bi uskoro započeti.', - nb: '{operationName:string} av dette datasettet skal snart starte.', + nb: '{operationName:string} av dette datasettet bør starte snart.', }, wbStatusPendingSecondDescription: { 'en-us': @@ -1657,13 +1679,13 @@ export const wbText = createDictionary({ 'ru-ru': 'Если это сообщение отображается дольше 30 секунд, процесс {operationName:string} занят другим набором данных. Пожалуйста, попробуйте снова позже.', 'es-es': - 'Si este mensaje persiste por más de 30 segundos, el proceso {operationName:string} está ocupado con otro Conjunto de Datos. Por favor inténtelo más tarde.', + 'Si este mensaje persiste durante más de 30 segundos, el proceso {operationName:string} está ocupado con otro conjunto de datos. Inténtelo de nuevo más tarde.', 'fr-fr': - 'Si ce message persiste plus de 30 secondes, le processus {operationName:string} est occupé avec un autre ensemble de données. Veuillez réessayer plus tard.', + "Si ce message persiste pendant plus de 30 secondes, le processus {operationName:string} est occupé par le traitement d'un autre ensemble de données. Veuillez réessayer plus tard.", 'uk-ua': 'Якщо це повідомлення зберігається довше 30 секунд, процес {operationName:string} зайнятий іншим набором даних. Будь ласка, спробуйте пізніше.', 'de-ch': - 'Wenn diese Meldung länger als 30 Sekunden angezeigt wird, ist der Prozess {operationName:string} mit einem anderen Datensatz beschäftigt. Bitte versuchen Sie es später erneut.', + 'Wenn diese Meldung länger als 30 Sekunden angezeigt wird, verarbeitet der Prozess {operationName:string} gerade einen anderen Datensatz. Bitte versuchen Sie es später erneut.', 'pt-br': 'Se esta mensagem persistir por mais de 30 segundos, o processo {operationName:string} está ocupado com outro conjunto de dados. Tente novamente mais tarde.', 'hr-hr': @@ -1833,29 +1855,29 @@ export const wbText = createDictionary({ }, recordsCreated: { 'en-us': 'New records', - 'de-ch': 'Datensätze erstellt', - 'es-es': 'Registros creados', - 'fr-fr': 'Enregistrements créés', - 'pt-br': 'Registros criados', - 'ru-ru': 'Созданы записи', - 'uk-ua': 'Створені записи', - 'hr-hr': 'Stvoreni zapisi', - nb: 'Opprettede poster', + 'de-ch': 'Neue Rekorde', + 'es-es': 'Nuevos récords', + 'fr-fr': 'Nouveaux records', + 'pt-br': 'Novos recordes', + 'ru-ru': 'Новые рекорды', + 'uk-ua': 'Нові рекорди', + 'hr-hr': 'Novi rekordi', + nb: 'Nye rekorder', }, recordsUpdated: { 'en-us': 'Updated records', 'es-es': 'Registros actualizados', - 'fr-fr': 'Enregistrements mis-à-jour', + 'fr-fr': 'Archives mises à jour', 'de-ch': 'Aktualisierte Datensätze', 'pt-br': 'Registros atualizados', - 'hr-hr': 'Zapisi ažurirani', - nb: 'Oppføringer oppdatert', - 'ru-ru': '', - 'uk-ua': '', + 'hr-hr': 'Ažurirani zapisi', + nb: 'Oppdaterte poster', + 'ru-ru': 'Обновлены записи', + 'uk-ua': 'Оновлені записи', }, recordsDeleted: { 'en-us': 'Records deleted (not including dependents)', - 'de-ch': 'Gelöschte Datensätze (ohne Abhängige)', + 'de-ch': 'Gelöschte Datensätze (ohne Angehörige)', 'es-es': 'Registros eliminados (sin incluir a los dependientes)', 'fr-fr': "Enregistrements supprimés (à l'exclusion des enregistrements dépendants)", From d53f8be06c2bccc863cdc55bc514883d3cb27a7c Mon Sep 17 00:00:00 2001 From: melton-jason Date: Thu, 9 Jul 2026 17:43:48 +0000 Subject: [PATCH 079/126] Sync localization strings with Weblate Triggered by 486526b0ca2d586abde321cd12249d3348abb9ed on branch refs/heads/weblate-localization --- .../js_src/lib/localization/batchEdit.ts | 20 +- .../frontend/js_src/lib/localization/forms.ts | 4 +- .../js_src/lib/localization/wbPlan.ts | 241 ++++++++++-------- 3 files changed, 141 insertions(+), 124 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/batchEdit.ts b/specifyweb/frontend/js_src/lib/localization/batchEdit.ts index 6adb15b6980..3f643767e33 100644 --- a/specifyweb/frontend/js_src/lib/localization/batchEdit.ts +++ b/specifyweb/frontend/js_src/lib/localization/batchEdit.ts @@ -234,20 +234,20 @@ export const batchEditText = createDictionary({ 'en-us': 'Your changes have been applied to the database. See the number of records affected in each table below:', 'de-ch': - 'Ihre Änderungen wurden in die Datenbank übernommen. Um die Anzahl der betroffenen Datensätze in jeder Tabelle anzuzeigen, klicken Sie in der Symbolleiste über dem Datenraster auf „Ergebnisse".', + 'Ihre Änderungen wurden in der Datenbank übernommen. Die Anzahl der betroffenen Datensätze in den einzelnen Tabellen finden Sie unten:', 'es-es': 'Sus cambios se han aplicado a la base de datos. Para ver el número de registros afectados en cada tabla, haga clic en "Resultados" en la barra de herramientas sobre la cuadrícula de datos.', 'fr-fr': "Vos modifications ont été appliquées à la base de données. Pour voir le nombre d'enregistrements affectés dans chaque table, cliquez sur « Résultats » dans la barre d'outils au-dessus de la grille de données.", 'pt-br': - 'Suas alterações foram aplicadas ao banco de dados. Para ver o número de registros afetados em cada tabela, clique em "Resultados" na barra de ferramentas acima da grade de dados.', + 'As suas alterações foram aplicadas à base de dados. Veja abaixo o número de registos afetados em cada tabela:', 'ru-ru': - 'Ваши изменения были применены к базе данных. Чтобы увидеть количество затронутых записей в каждой таблице, нажмите «Результаты» на панели инструментов над сеткой данных.', + 'Ваши изменения были применены к базе данных. Ниже вы можете увидеть количество затронутых записей в каждой таблице:', 'uk-ua': 'Ваші зміни було застосовано до бази даних. Щоб переглянути кількість записів, на які вплинули зміни, у кожній таблиці, натисніть «Результати» на панелі інструментів над сіткою даних.', 'hr-hr': - 'Kliknite gumb "Rezultati" da biste vidjeli broj pogođenih zapisa u svakoj tablici baze podataka', - nb: 'Klikk på «Resultater»-knappen for å se antall treff som blir berørt i hver databasetabell', + 'Vaše su promjene primijenjene u bazi podataka. Pogledajte broj pogođenih zapisa u svakoj tablici u nastavku:', + nb: 'Endringene dine er implementert i databasen. Se antall berørte poster i hver tabell nedenfor:', }, dateSetRevertDescription: { 'en-us': @@ -523,17 +523,17 @@ export const batchEditText = createDictionary({ 'de-ch': 'Bevor Sie fortfahren, beachten Sie bitte, dass die folgende Aktion andere Nutzer beeinträchtigen kann. Dies kann zu Verzögerungen oder vorübergehender Nichtverfügbarkeit bestimmter Funktionen für bestimmte Nutzer führen. Bitte berücksichtigen Sie die Auswirkungen auf deren Nutzungserfahrung. Diese Aktion kann nicht rückgängig gemacht werden.', 'es-es': - 'Antes de continuar, tenga en cuenta que la siguiente acción podría interrumpir a otros usuarios. Esta acción podría causar retrasos o la indisponibilidad temporal de ciertas funciones para los usuarios especificados. Considere el impacto en su experiencia. Esta acción no se puede deshacer.', + 'Antes de continuar, tenga en cuenta que la siguiente acción podría interrumpir a otros usuarios. Esta acción podría ocasionar retrasos o la indisponibilidad temporal de ciertas funciones para los usuarios especificados. Considere el impacto en su experiencia. Esta acción es irreversible.', 'fr-fr': - "Avant de continuer, veuillez noter que l'action suivante peut perturber d'autres utilisateurs si effectuée sur un grand jeu de données. Cette action peut entraîner des lenteurs ou l'indisponibilité temporaire de certaines fonctionnalités. Cette action est irréversible.", + "Avant de poursuivre, veuillez noter que l'action suivante peut perturber d'autres utilisateurs. Cette action peut entraîner des retards ou l'indisponibilité temporaire de certaines fonctionnalités pour les utilisateurs de Specific. Veuillez tenir compte de l'impact sur leur expérience. Cette action est irréversible.", 'pt-br': - 'Antes de prosseguir, observe que a ação a seguir pode interromper outros usuários. Esta ação pode causar atrasos ou indisponibilidade temporária de certos recursos para os usuários especificados. Considere o impacto na experiência deles. Esta ação não pode ser desfeita.', + 'Antes de prosseguir, observe que a ação a seguir pode interromper outros usuários. Essa ação pode causar atrasos ou indisponibilidade temporária de certos recursos para usuários específicos. Considere o impacto na experiência deles. Esta ação não pode ser desfeita.', 'ru-ru': 'Прежде чем продолжить, обратите внимание, что следующее действие может помешать другим пользователям. Это действие может вызвать задержки или временную недоступность некоторых функций для указанных пользователей. Пожалуйста, учтите влияние на их опыт использования сайта. Это действие необратимо.', 'uk-ua': - 'Перш ніж продовжити, зверніть увагу, що наступна дія може перешкодити іншим користувачам. Ця дія може спричинити затримки або тимчасову недоступність певних функцій для певних користувачів. Будь ласка, врахуйте вплив на їхній досвід. Цю дію не можна скасувати', + 'Перш ніж продовжити, зверніть увагу, що наступна дія може перешкодити іншим користувачам. Ця дія може спричинити затримки або тимчасову недоступність певних функцій для визначених користувачів. Будь ласка, врахуйте вплив на їхній досвід. Цю дію не можна скасувати.', 'hr-hr': 'Prije nego što nastavite, imajte na umu da sljedeća radnja može poremetiti rad drugih korisnika. Ova radnja može uzrokovati kašnjenja ili privremenu nedostupnost određenih značajki za određene korisnike. Molimo vas da uzmete u obzir utjecaj na njihovo iskustvo. Ova se radnja ne može poništiti.', - nb: 'Før du fortsetter, vær oppmerksom på at denne handlingen kan påvirke andre brukere. Den kan føre til forsinkelser eller midlertidig utilgjengelighet av enkelte funksjoner i Specify. Vennligst vurder konsekvensene for brukeropplevelsen. Handlingen kan ikke angres.', + nb: 'Før du fortsetter, vær oppmerksom på at følgende handling kan forstyrre andre brukere. Denne handlingen kan forårsake forsinkelser eller midlertidig utilgjengelighet av enkelte funksjoner for Specify-brukere. Vurder hvordan dette påvirker brukeropplevelsen deres. Denne handlingen kan ikke angres.', }, } as const); diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index 4d5505ed102..efa196bb426 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -447,7 +447,7 @@ export const formsText = createDictionary({ 'de-ch': 'Letzter Datensatz', 'pt-br': 'Último registro', 'hr-hr': 'Posljednji zapis', - nb: 'Siste post', + nb: 'Siste oppføring', }, previousRecord: { 'en-us': 'Previous Record', @@ -1108,7 +1108,7 @@ export const formsText = createDictionary({ 'ru-ru': 'Настройка полей для массового переноса ({tableName:string})', 'uk-ua': 'Налаштуйте поля для масового перенесення ({tableName:string})', 'hr-hr': 'Konfigurirajte polja za skupni prijenos ({tableName:string})', - nb: 'Konfigurer felt for masseoverføring ({tableName:string})', + nb: 'Konfigurer felt for masseoverføring fremover ({tableName:string})', }, carryForwardUniqueField: { 'en-us': 'This field must be unique. It can not be carried over', diff --git a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts index c4360a073b5..af258de00e2 100644 --- a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts +++ b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts @@ -22,56 +22,63 @@ export const wbPlanText = createDictionary({ }, importExportMapping: { 'en-us': 'Import/Export Mapping', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', + 'de-ch': 'Import-/Export-Zuordnung', + 'es-es': 'Mapeo de importación/exportación', + 'fr-fr': "Cartographie d'import/export", + 'hr-hr': 'Mapiranje uvoza/izvoza', + nb: 'Importer/eksporter kartlegging', + 'pt-br': 'Mapeamento de Importação/Exportação', + 'ru-ru': 'Сопоставление импорта/экспорта', + 'uk-ua': 'Зіставлення імпорту/експорту', }, importExportMappingDescription: { 'en-us': 'You can export the current data set mapping as a JSON file or import an existing data set mapping.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', + 'de-ch': + 'Sie können die aktuelle Datensatzzuordnung als JSON-Datei exportieren oder eine bestehende Datensatzzuordnung importieren.', + 'es-es': + 'Puede exportar la asignación del conjunto de datos actual como un archivo JSON o importar una asignación de conjunto de datos existente.', + 'fr-fr': + 'Vous pouvez exporter le mappage de jeu de données actuel sous forme de fichier JSON ou importer un mappage de jeu de données existant.', + 'hr-hr': + 'Možete izvesti trenutno mapiranje skupa podataka kao JSON datoteku ili uvesti postojeće mapiranje skupa podataka.', + nb: 'Du kan eksportere den gjeldende datasetttilordningen som en JSON-fil eller importere en eksisterende datasetttilordning.', + 'pt-br': + 'Você pode exportar o mapeamento do conjunto de dados atual como um arquivo JSON ou importar um mapeamento de conjunto de dados existente.', + 'ru-ru': + 'Вы можете экспортировать текущее сопоставление наборов данных в файл JSON или импортировать существующее сопоставление наборов данных.', + 'uk-ua': + 'Ви можете експортувати поточне зіставлення набору даних як файл JSON або імпортувати існуюче зіставлення набору даних.', }, noUploadPlan: { 'en-us': 'No Data Set Mapping is Defined', - 'ru-ru': 'План загрузки не определен', - 'es-es': 'No hay definido ningún plan de carga', - 'fr-fr': "Aucun plan de téléchargement n'est défini.", - 'uk-ua': 'План завантаження не визначено', - 'de-ch': 'Es wurde kein Uploadplan definiert', - 'pt-br': 'Nenhum plano de upload foi definido.', - 'hr-hr': 'Nije definiran plan prijenosa', - nb: 'Ingen opplastingsplan er definert', + 'ru-ru': 'Сопоставление наборов данных не определено.', + 'es-es': 'No se ha definido ninguna asignación de conjuntos de datos.', + 'fr-fr': "Aucun mappage de jeu de données n'est défini.", + 'uk-ua': 'Відображення набору даних не визначено', + 'de-ch': 'Es ist keine Datensatzzuordnung definiert.', + 'pt-br': 'Nenhum mapeamento de conjunto de dados foi definido.', + 'hr-hr': 'Nije definirano mapiranje skupa podataka', + nb: 'Ingen datasetttilordning er definert', }, noUploadPlanDescription: { 'en-us': 'No mapping has been defined for this data set. Please choose an existing mapping or create a new one.', 'ru-ru': - 'Для этого набора данных не определен план загрузки. Создать эго сейчас?', + 'Для этого набора данных не определено сопоставление. Пожалуйста, выберите существующее сопоставление или создайте новое.', 'es-es': - 'No se ha definido ningún plan de carga para este conjunto de datos. ¿Crear uno ahora?', + 'No se ha definido ninguna asignación para este conjunto de datos. Por favor, seleccione una asignación existente o cree una nueva.', 'fr-fr': - "Aucun plan de chargement n'a été défini pour cet ensemble de données. En créer un maintenant ?", + "Aucun mappage n'a été défini pour cet ensemble de données. Veuillez choisir un mappage existant ou en créer un nouveau.", 'uk-ua': - 'Для цього набору даних не визначено план завантаження. Створити зараз?', + 'Для цього набору даних не визначено жодного зіставлення. Виберіть існуюче зіставлення або створіть нове.', 'de-ch': - 'Für diesen Datensatz wurde noch kein Upload-Plan definiert. Jetzt einen erstellen?', + 'Für diesen Datensatz ist keine Zuordnung definiert. Bitte wählen Sie eine vorhandene Zuordnung aus oder erstellen Sie eine neue.', 'pt-br': - 'Nenhum plano de upload foi definido para este conjunto de dados. Deseja criar um agora?', + 'Não existe um mapeamento definido para este conjunto de dados. Por favor, escolha um mapeamento existente ou crie um novo.', 'hr-hr': - 'Za ovaj skup podataka nije definiran plan prijenosa. Želite li ga sada izraditi?', - nb: 'Ingen opplastingsplan er definert for dette datasettet. Opprette en nå?', + 'Za ovaj skup podataka nije definirano mapiranje. Odaberite postojeće mapiranje ili stvorite novo.', + nb: 'Ingen kartlegging er definert for dette datasettet. Vennligst velg en eksisterende kartlegging eller opprett en ny.', }, unmappedColumn: { 'en-us': 'Unmapped Column', @@ -251,20 +258,20 @@ export const wbPlanText = createDictionary({ 'en-us': 'When set to "Always Ignore," the value in this column will not be used for matching purposes, only for uploading.', 'ru-ru': - 'Если задано значение «Всегда игнорировать», значение в этом столбце никогда не будет рассматривается для целей сопоставления, только для загрузки', + 'Если установлено значение "Всегда игнорировать", значение в этом столбце не будет использоваться для сопоставления, а только для загрузки.', 'es-es': - 'Cuando se establece "Ignorar siempre", el valor de esta columna nunca se tomará en cuenta a efectos de comparación; solo al cargar datos.', + 'Cuando se selecciona "Ignorar siempre", el valor de esta columna no se utilizará para fines de comparación, sino únicamente para la carga de archivos.', 'fr-fr': - "Si l'option « Ignorer toujours » est sélectionnée, la valeur de cette colonne ne sera jamais prise en compte pour la mise en correspondance, mais uniquement pour le chargement.", + "Si l'option « Toujours ignorer » est sélectionnée, la valeur de cette colonne ne sera pas utilisée à des fins de correspondance, mais uniquement pour le chargement.", 'uk-ua': - 'Якщо встановлено значення «Ігнорувати завжди», значення в цьому стовпці ніколи не розглядатиметься для цілей зіставлення, лише для завантаження.', + 'Якщо встановлено значення «Завжди ігнорувати», значення в цьому стовпці не використовуватиметься для зіставлення, а лише для завантаження.', 'de-ch': - 'Bei der Einstellung "Immer ignorieren" wird der Wert in dieser Spalte niemals für den Abgleich, sondern nur für das Hochladen berücksichtigt.', + 'Wenn die Option „Immer ignorieren“ ausgewählt ist, wird der Wert in dieser Spalte nicht für Abgleichzwecke, sondern nur für den Upload verwendet.', 'pt-br': - 'Quando definida como "Ignorar Sempre", o valor nesta coluna nunca será considerado para fins de correspondência, apenas para carregamento.', + 'Quando definida como "Ignorar sempre", o valor nesta coluna não será usado para fins de correspondência, apenas para carregamento.', 'hr-hr': - 'Kada je postavljeno na "Uvijek zanemari", vrijednost u ovom stupcu nikada se neće uzimati u obzir za potrebe podudaranja, već samo za prijenos.', - nb: 'Når den er satt til «Ignorer alltid», vil verdien i denne kolonnen aldri bli vurdert for samsvarsformål, kun for opplasting.', + 'Kada je postavljeno na "Uvijek zanemari", vrijednost u ovom stupcu neće se koristiti za potrebe podudaranja, već samo za prijenos.', + nb: 'Når den er satt til «Ignorer alltid», vil verdien i denne kolonnen ikke bli brukt til samsvarsformål, kun til opplasting.', }, ignoreNever: { 'en-us': 'Never Ignore', @@ -366,20 +373,20 @@ export const wbPlanText = createDictionary({ 'en-us': 'This data mapping is missing one or more data fields required for uploading by your Specify configuration. Add the missing mappings shown or save this mapping as unfinished.', 'ru-ru': - 'В этом сопоставлении данные отсутствует в одном или нескольких полей данных, необходимых для загрузки по вашей Specify конфигурацию. Добавьте недостающие сопоставления или сохраните этот план загрузки как незавершенный.', + 'В этом сопоставлении данных отсутствует одно или несколько полей, необходимых для загрузки в соответствии с вашей конфигурацией Specify. Добавьте недостающие сопоставления, указанные выше, или сохраните это сопоставление как незавершенное.', 'es-es': - 'A este mapeo de datos le faltan uno o más campos de datos requeridos para cargar por su configuración de Especificar. Agregue las asignaciones faltantes que se muestran o guarde este plan de carga como inacabado.', + 'A esta asignación de datos le faltan uno o más campos de datos necesarios para la carga según su configuración de Specify. Agregue las asignaciones faltantes que se muestran o guarde esta asignación como incompleta.', 'fr-fr': - 'Il manque un ou plusieurs mappings requis pour le chargement selon votre configuration. Ajoutez les mappings manquants ou enregistrez ce plan de chargement comme inachevé.', + 'Il manque un ou plusieurs champs de données requis pour le chargement selon votre configuration. Ajoutez les correspondances manquantes indiquées ou enregistrez cette correspondance comme inachevée.', 'uk-ua': - 'У цьому відображенні даних відсутнє одне або кілька полів даних, необхідні для завантаження вашою конфігурацією Specify. Додайте відсутні відображення або збережіть цей план завантаження як незавершений.', + 'У цьому зіставленні даних відсутнє одне або кілька полів даних, необхідних для завантаження згідно з вашою конфігурацією Specify. Додайте відсутні зіставлення або збережіть це зіставлення як незавершене.', 'de-ch': - 'In dieser Datenzuordnung fehlen ein oder mehrere Datenfelder, die für das Hochladen gemäss Ihrer Specify-Konfiguration erforderlich sind. Fügen Sie die fehlenden Mappings hinzu oder speichern Sie diesen Upload-Plan als unvollendet.', + 'Diese Datenzuordnung weist ein oder mehrere fehlende Datenfelder auf, die gemäß Ihrer Specify-Konfiguration für den Upload erforderlich sind. Fügen Sie die fehlenden Zuordnungen hinzu oder speichern Sie diese Zuordnung als unvollständig.', 'pt-br': - 'Este mapeamento de dados está incompleto, faltando um ou mais campos de dados necessários para o carregamento de acordo com a sua configuração. Adicione os mapeamentos ausentes mostrados ou salve este Plano de Carregamento como incompleto.', + 'Este mapeamento de dados está incompleto, faltando um ou mais campos de dados necessários para o carregamento de acordo com a sua configuração. Adicione os mapeamentos ausentes mostrados ou salve este mapeamento como incompleto.', 'hr-hr': - 'Ovom mapiranju podataka nedostaje jedno ili više podatkovnih polja potrebnih za prijenos prema vašoj konfiguraciji Navedite. Dodajte prikazana mapiranja koja nedostaju ili spremite ovaj plan prijenosa kao nedovršen.', - nb: 'Denne datatilordningen mangler ett eller flere datafelt som kreves for opplasting av din Spesifiser-konfigurasjon. Legg til de manglende tilordningene som vises, eller lagre denne opplastingsplanen som uferdig.', + 'Ovom mapiranju podataka nedostaje jedno ili više podatkovnih polja potrebnih za učitavanje prema vašoj konfiguraciji Navedi. Dodajte prikazana mapiranja koja nedostaju ili spremite ovo mapiranje kao nedovršeno.', + nb: 'Denne datatilordningen mangler ett eller flere datafelt som kreves for opplasting av din Spesifiser-konfigurasjon. Legg til de manglende tilordningene som vises, eller lagre denne tilordningen som uferdig.', }, mappingIsRequired: { comment: 'I.e, this field must be mapped before you can continue', @@ -472,14 +479,14 @@ export const wbPlanText = createDictionary({ }, chooseExistingPlan: { 'en-us': 'Choose Existing Mapping', - 'ru-ru': 'Выберите существующий план', - 'es-es': 'Elegir un Plan ya Existente', - 'fr-fr': 'Choisir un plan de téléchargement existant', - 'uk-ua': 'Виберіть існуючий план', - 'de-ch': 'Bestehenden Plan auswählen', - 'pt-br': 'Escolha um plano existente', - 'hr-hr': 'Odaberite postojeći plan', - nb: 'Velg eksisterende plan', + 'ru-ru': 'Выберите существующее сопоставление', + 'es-es': 'Seleccionar un mapa existente', + 'fr-fr': 'Choisir une cartographie existante', + 'uk-ua': 'Виберіть існуюче відображення', + 'de-ch': 'Vorhandene Zuordnung auswählen', + 'pt-br': 'Selecionar mapeamento existente', + 'hr-hr': 'Odaberite postojeće mapiranje', + nb: 'Velg eksisterende kartlegging', }, showAllTables: { 'en-us': 'Show All Tables', @@ -495,26 +502,40 @@ export const wbPlanText = createDictionary({ baseTableDescription: { 'en-us': "A 'base table' is the table that serves as the starting point for column-to-data field mappings. Once uploaded, each row in your data set will result in a new record in Specify in the base table you select. Click on a base table in the list to get started.", - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', + 'de-ch': + 'Eine „Basistabelle“ dient als Ausgangspunkt für die Zuordnung von Spalten zu Datenfeldern. Nach dem Hochladen wird jede Zeile Ihres Datensatzes in der von Ihnen ausgewählten Basistabelle als neuer Datensatz angelegt. Klicken Sie in der Liste auf eine Basistabelle, um zu beginnen.', + 'es-es': + 'Una «tabla base» es la tabla que sirve como punto de partida para la asignación de columnas a campos de datos. Una vez cargada, cada fila de su conjunto de datos generará un nuevo registro en la tabla base que seleccione. Haga clic en una tabla base de la lista para comenzar.', + 'fr-fr': + 'Une « table de base » sert de point de départ pour la correspondance entre les colonnes et les champs de données. Une fois importée, chaque ligne de votre jeu de données créera un nouvel enregistrement dans la table de base sélectionnée. Cliquez sur une table de base dans la liste pour commencer.', + 'hr-hr': + "'Osnovna tablica' je tablica koja služi kao početna točka za mapiranje stupaca u podatkovna polja. Nakon prijenosa, svaki redak u vašem skupu podataka rezultirat će novim zapisom u . Navedite u odabranoj osnovnoj tablici. Kliknite na osnovnu tablicu na popisu da biste započeli.", + nb: 'En «basistabell» er tabellen som fungerer som utgangspunkt for tilordninger mellom kolonner og datafelt. Når den er lastet opp, vil hver rad i datasettet resultere i en ny post i Spesifiser i basistabellen du velger. Klikk på en basistabell i listen for å komme i gang.', + 'pt-br': + "Uma 'tabela base' é a tabela que serve como ponto de partida para o mapeamento de colunas para campos de dados. Após o carregamento, cada linha do seu conjunto de dados resultará em um novo registro na tabela base que você selecionar. Clique em uma tabela base na lista para começar.", + 'ru-ru': + '«Базовая таблица» — это таблица, которая служит отправной точкой для сопоставления столбцов с полями данных. После загрузки каждая строка в вашем наборе данных будет приводить к созданию новой записи в указанной вами базовой таблице. Щелкните по базовой таблице в списке, чтобы начать.', + 'uk-ua': + '«Базова таблиця» – це таблиця, яка слугує відправною точкою для зіставлення стовпців із полями даних. Після завантаження кожен рядок у вашому наборі даних призведе до створення нового запису в . Укажіть у вибраній базовій таблиці. Натисніть на базову таблицю у списку, щоб розпочати.', }, baseTableWithAttachmentsDescription: { 'en-us': "A 'base table' is the table that serves as the starting point for column-to-data field mappings. Each imported attachment record will be added as a new row in the base table you select. Click on a base table in the list to get started.", - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', + 'de-ch': + 'Eine „Basistabelle“ dient als Ausgangspunkt für die Zuordnung von Spalten zu Datenfeldern. Jeder importierte Anhangsdatensatz wird als neue Zeile in die ausgewählte Basistabelle eingefügt. Klicken Sie in der Liste auf eine Basistabelle, um zu beginnen.', + 'es-es': + 'Una tabla base sirve como punto de partida para la asignación de columnas a campos de datos. Cada registro de archivo adjunto importado se añadirá como una nueva fila en la tabla base que seleccione. Haga clic en una tabla base de la lista para comenzar.', + 'fr-fr': + 'Une « table de base » sert de point de départ pour la correspondance entre les colonnes et les champs de données. Chaque enregistrement de pièce jointe importée sera ajouté comme une nouvelle ligne dans la table de base sélectionnée. Cliquez sur une table de base dans la liste pour commencer.', + 'hr-hr': + "'Osnovna tablica' je tablica koja služi kao početna točka za mapiranje stupaca u podatkovna polja. Svaki uvezeni zapis privitka bit će dodan kao novi redak u odabranoj osnovnoj tablici. Kliknite na osnovnu tablicu na popisu da biste započeli.", + nb: 'En «basistabell» er tabellen som fungerer som utgangspunkt for tilordninger mellom kolonner og datafelt. Hver importerte vedleggspost legges til som en ny rad i basistabellen du velger. Klikk på en basistabell i listen for å komme i gang.', + 'pt-br': + "Uma 'tabela base' é a tabela que serve como ponto de partida para o mapeamento de colunas para campos de dados. Cada registro de anexo importado será adicionado como uma nova linha na tabela base selecionada. Clique em uma tabela base na lista para começar.", + 'ru-ru': + '«Базовая таблица» — это таблица, которая служит отправной точкой для сопоставления столбцов с полями данных. Каждая импортированная запись вложения будет добавлена в выбранную вами базовую таблицу в виде новой строки. Щелкните по базовой таблице в списке, чтобы начать.', + 'uk-ua': + '«Базова таблиця» – це таблиця, яка слугує відправною точкою для зіставлення стовпців із полями даних. Кожен імпортований вкладений запис буде додано як новий рядок у вибрану базову таблицю. Щоб розпочати, натисніть на базову таблицю у списку.', }, selectBaseTableWithAttachments: { 'en-us': 'Select a Base Table with Attachments', @@ -529,19 +550,16 @@ export const wbPlanText = createDictionary({ }, dataSetUploaded: { 'en-us': 'Data Set uploaded. This mapping cannot be changed', - 'ru-ru': 'Набор данных загружен. Этот план загрузки нельзя изменить', - 'es-es': - 'Conjunto de Datos cargado. El Plan de Carga ya no puede modificarse', - 'fr-fr': - 'Jeu de données téléchargé. Ce plan de téléchargement ne peut pas être modifié.', - 'uk-ua': 'Набір даних завантажено. Цей план завантаження не можна змінити', + 'ru-ru': 'Набор данных загружен. Это сопоставление изменить нельзя.', + 'es-es': 'Conjunto de datos cargado. Este mapeo no se puede modificar.', + 'fr-fr': 'Jeu de données téléchargé. Ce mappage ne peut pas être modifié.', + 'uk-ua': 'Набір даних завантажено. Це зіставлення не можна змінити.', 'de-ch': - 'Datensatz hochgeladen. Dieser Upload-Plan kann nicht geändert werden', + 'Datensatz hochgeladen. Diese Zuordnung kann nicht geändert werden.', 'pt-br': - 'Conjunto de dados carregado. Este plano de carregamento não pode ser alterado.', - 'hr-hr': - 'Skup podataka prenesen. Ovaj plan prijenosa ne može se promijeniti.', - nb: 'Datasettet er lastet opp. Denne opplastingsplanen kan ikke endres.', + 'Conjunto de dados carregado. Este mapeamento não pode ser alterado.', + 'hr-hr': 'Skup podataka prenesen. Ovo mapiranje se ne može promijeniti.', + nb: 'Datasettet er lastet opp. Denne kartleggingen kan ikke endres.', }, dataSetUploadedDescription: { 'en-us': @@ -593,20 +611,20 @@ export const wbPlanText = createDictionary({ 'en-us': 'Choosing a different base table for a data set will make that table the new starting point for column-to-data field mappings and will erase existing mappings. The AutoMapper will attempt to map columns to the new base table fields.', 'ru-ru': - 'Выбор другой базовой таблице для загрузки набора данных сделает ту таблицу новой отправной точкой для сопоставлений полей столбцов и данных и сотрет существующие сопоставления. AutoMapper попытается сопоставить столбцы в новые поля базовой таблицы.', + 'Выбор другой базовой таблицы для набора данных сделает эту таблицу новой отправной точкой для сопоставления столбцов с полями данных и удалит существующие сопоставления. AutoMapper попытается сопоставить столбцы с полями новой базовой таблицы.', 'es-es': - 'Si elige una tabla base diferente para la carga de un conjunto de datos, esa tabla se convertirá en el nuevo punto de partida para las asignaciones de campo de columna a datos y borrará las asignaciones existentes. El AutoMapper intentará asignar columnas a los nuevos campos de la tabla base.', + 'Al seleccionar una tabla base diferente para un conjunto de datos, dicha tabla se convertirá en el nuevo punto de partida para la asignación de columnas a campos de datos y se borrarán las asignaciones existentes. El AutoMapper intentará asignar las columnas a los campos de la nueva tabla base.', 'fr-fr': - "Choisir une autre table de base pour l'importation d'un jeu de données définira cette table comme nouveau point de départ pour la correspondance entre les colonnes et les champs de données, et effacera les correspondances existantes. L'outil de mappage automatique tentera d'associer les colonnes aux champs de la nouvelle table de base.", + "Choisir une autre table de base pour un ensemble de données définira cette table comme nouveau point de départ pour la correspondance entre les colonnes et les champs de données, et effacera les correspondances existantes. L'outil de mappage automatique tentera d'associer les colonnes aux champs de la nouvelle table de base.", 'uk-ua': - 'Вибір іншої базової таблиці для завантаження набору даних зробить цю таблицю новою відправною точкою для зіставлення стовпців і полів даних і видалить існуючі зіставлення. AutoMapper спробує зіставити стовпці з новими полями базової таблиці.', + 'Вибір іншої базової таблиці для набору даних зробить цю таблицю новою відправною точкою для зіставлення стовпців з полями даних та видалить існуючі зіставлення. AutoMapper спробує зіставити стовпці з новими полями базової таблиці.', 'de-ch': - 'Durch Auswahl einer anderen Basistabelle für einen Datensatz-Upload wird diese Tabelle zum neuen Ausgangspunkt für die Zuordnung von Spalten zu Datenfeldern und die bestehenden Zuordnungen werden gelöscht. Der AutoMapper wird versuchen, die Spalten den neuen Basistabellenfeldern zuzuordnen.', + 'Wenn Sie eine andere Basistabelle für einen Datensatz auswählen, wird diese Tabelle zum neuen Ausgangspunkt für die Zuordnung von Spalten zu Datenfeldern und die bestehenden Zuordnungen werden gelöscht. Der AutoMapper versucht dann, die Spalten den Feldern der neuen Basistabelle zuzuordnen.', 'pt-br': - 'Ao escolher uma tabela base diferente para o carregamento de um conjunto de dados, essa tabela se tornará o novo ponto de partida para o mapeamento de colunas para campos de dados e apagará os mapeamentos existentes. O AutoMapper tentará mapear as colunas para os campos da nova tabela base.', + 'Escolher uma tabela base diferente para um conjunto de dados fará com que essa tabela se torne o novo ponto de partida para o mapeamento de colunas para campos de dados e apagará os mapeamentos existentes. O AutoMapper tentará mapear as colunas para os campos da nova tabela base.', 'hr-hr': - 'Odabirom druge osnovne tablice za prijenos skupa podataka ta će tablica postati nova početna točka za mapiranje stupaca u podatkovna polja i izbrisat će se postojeća mapiranja. AutoMapper će pokušati mapirati stupce na nova polja osnovne tablice.', - nb: 'Hvis du velger en annen basistabell for opplasting av datasett, blir den tabellen det nye startpunktet for tilordninger mellom kolonner og datafelt, og eksisterende tilordninger slettes. AutoMapper vil forsøke å tilordne kolonner til de nye basistabellfeltene.', + 'Odabirom druge osnovne tablice za skup podataka ta će tablica postati nova početna točka za mapiranja stupaca u podatkovna polja i izbrisat će se postojeća mapiranja. AutoMapper će pokušati mapirati stupce na nova polja osnovne tablice.', + nb: 'Hvis du velger en annen basistabell for et datasett, blir den tabellen det nye startpunktet for tilordninger mellom kolonner og datafelt, og eksisterende tilordninger slettes. AutoMapper vil forsøke å tilordne kolonner til de nye basistabellfeltene.', }, clearMapping: { 'en-us': 'Clear Mapping', @@ -739,15 +757,14 @@ export const wbPlanText = createDictionary({ }, reRunAutoMapperConfirmation: { 'en-us': 'Automap to start a new mapping?', - 'ru-ru': 'Автоматически сопоставить?', - 'es-es': '¿Automap para iniciar un nuevo plan de carga?', - 'de-ch': 'Automap, um einen neuen Upload-Plan zu starten?', - 'fr-fr': - 'Mapping automatique pour démarrer un nouveau plan de téléchargement ?', - 'uk-ua': 'Автоматична карта, щоб почати новий план завантаження?', - 'pt-br': 'Automap para iniciar um novo Plano de Upload?', - 'hr-hr': 'Automatsko mapiranje za pokretanje novog plana prijenosa?', - nb: 'For å starte en ny opplastingsplan med automatisk tilordning?', + 'ru-ru': 'Чтобы начать создание новой карты с помощью функции Automap?', + 'es-es': '¿Automap inicia un nuevo mapeo?', + 'de-ch': 'Automap soll eine neue Kartierung starten?', + 'fr-fr': 'Automap pour démarrer une nouvelle cartographie ?', + 'uk-ua': 'Автоматичне створення карти, щоб розпочати нове відображення?', + 'pt-br': 'Automapa para iniciar um novo mapeamento?', + 'hr-hr': 'Automatsko mapiranje za početak novog mapiranja?', + nb: 'For å starte en ny kartlegging med Automap?', }, reRunAutoMapperConfirmationDescription: { 'en-us': 'This will erase existing data field mappings.', @@ -769,7 +786,7 @@ export const wbPlanText = createDictionary({ 'de-ch': 'Alle bestehenden Zuordnungen löschen?', 'pt-br': 'Limpar todos os mapeamentos existentes?', 'hr-hr': 'Izbrisati sva postojeća mapiranja?', - nb: '', + nb: 'Fjern alle eksisterende tilordninger?', }, clearMappingsConfirmationDescription: { 'en-us': 'This will erase existing data field mappings.', @@ -780,7 +797,7 @@ export const wbPlanText = createDictionary({ 'de-ch': 'Damit werden bestehende Zuordnungen von Datenfeldern gelöscht.', 'pt-br': 'Isso apagará os mapeamentos de campos de dados existentes.', 'hr-hr': 'Ovo će izbrisati postojeća mapiranja podatkovnih polja.', - nb: '', + nb: 'Dette vil slette eksisterende datafelttilordninger.', }, changeMatchingLogic: { 'en-us': 'Change Matching Logic', @@ -894,18 +911,18 @@ export const wbPlanText = createDictionary({ invalidTemplatePlan: { 'en-us': 'Selected data set has no mapping. Please select a different one.', 'ru-ru': - 'Выбранный набор данных не имеет плана загрузки. Выберите другой набор данных.', + 'Для выбранного набора данных отсутствует сопоставление. Пожалуйста, выберите другой набор.', 'es-es': - 'El conjunto de datos seleccionado no tiene un plan de carga. Seleccione uno diferente.', + 'El conjunto de datos seleccionado no tiene correspondencia. Seleccione otro.', 'fr-fr': - "L'ensemble de données sélectionné ne dispose d'aucun plan de chargement. Veuillez en sélectionner un autre.", - 'uk-ua': 'Вибраний набір даних не має плану завантаження. Виберіть інший.', + "L'ensemble de données sélectionné ne possède aucune correspondance. Veuillez en sélectionner un autre.", + 'uk-ua': 'Вибраний набір даних не має зіставлення. Виберіть інший.', 'de-ch': - 'Das ausgewählte Datenset hat keinen Upload-Plan. Bitte wählen Sie einen anderen Plan.', + 'Der ausgewählte Datensatz hat keine Zuordnung. Bitte wählen Sie einen anderen aus.', 'pt-br': - 'O conjunto de dados selecionado não possui um plano de upload. Selecione outro.', - 'hr-hr': 'Odabrani skup podataka nema plan prijenosa. Odaberite drugi.', - nb: 'Det valgte datasettet har ingen opplastingsplan. Vennligst velg en annen.', + 'O conjunto de dados selecionado não possui mapeamento. Selecione outro.', + 'hr-hr': 'Odabrani skup podataka nema mapiranje. Odaberite drugi.', + nb: 'Det valgte datasettet har ingen kartlegging. Vennligst velg et annet.', }, invalidJsonFile: { 'en-us': 'The selected file is not valid JSON.', @@ -916,7 +933,7 @@ export const wbPlanText = createDictionary({ 'de-ch': 'Die ausgewählte Datei ist kein gültiges JSON.', 'pt-br': 'O arquivo selecionado não é um JSON válido.', 'hr-hr': 'Odabrana datoteka nije valjani JSON.', - nb: '', + nb: 'Den valgte filen er ikke gyldig JSON.', }, invalidJsonFileDescription: { 'en-us': 'Please select a valid JSON data set mapping file.', @@ -927,6 +944,6 @@ export const wbPlanText = createDictionary({ 'de-ch': 'Bitte wählen Sie eine gültige JSON-Datei aus.', 'pt-br': 'Por favor, selecione um arquivo JSON válido.', 'hr-hr': 'Odaberite valjani JSON file.', - nb: '', + nb: 'Velg en gyldig JSON-datasetttilordningsfil.', }, } as const); From f996b9ff3fe498da99e6b1437a470fb0b0a38ad2 Mon Sep 17 00:00:00 2001 From: Google Translate Date: Thu, 9 Jul 2026 19:08:54 +0000 Subject: [PATCH 080/126] Sync localization strings with Weblate Triggered by 1a9b2f9aa49c5067f7b0d005fa3fd2d40f82e5b7 on branch refs/heads/weblate-localization --- .../js_src/lib/localization/preferences.ts | 16 +++---- .../js_src/lib/localization/welcome.ts | 44 ++++++++++++------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index 2f47d0776ce..67cbd4ec1a7 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -879,14 +879,14 @@ export const preferencesText = createDictionary({ }, displayChronoPeriods: { 'en-us': 'Show start and end periods', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', + 'de-ch': 'Start- und Endzeitraum anzeigen', + 'es-es': 'Mostrar los períodos de inicio y fin', + 'fr-fr': 'Afficher les périodes de début et de fin', + 'hr-hr': 'Prikaži početno i završno razdoblje', + nb: 'Vis start- og sluttperioder', + 'pt-br': 'Mostrar períodos de início e fim', + 'ru-ru': 'Показать начальный и конечный периоды', + 'uk-ua': 'Показати початковий та кінцевий періоди', }, welcomePage: { 'en-us': 'Home Page', diff --git a/specifyweb/frontend/js_src/lib/localization/welcome.ts b/specifyweb/frontend/js_src/lib/localization/welcome.ts index 6586d2559a8..742cc0bd10c 100644 --- a/specifyweb/frontend/js_src/lib/localization/welcome.ts +++ b/specifyweb/frontend/js_src/lib/localization/welcome.ts @@ -94,28 +94,40 @@ export const welcomeText = createDictionary({ disclosure: { 'en-us': 'Specify is developed by the Specify Collections Consortium (SCC), a collaborative initiative governed by its members and supported by institutional partners. Software support and development are made possible by consortium members, including the UniMus:Natur Consortium of Norway, the Commonwealth Scientific and Industrial Research Organisation (CSIRO), the Consejo Superior de Investigaciones Científicas (CSIC), the Denmark Consortium of Museums, the Muséum d’Histoire Naturelle Geneva, the University of Florida, the University of Kansas, and the University of Michigan, along with numerous other member collections and institutions within the Consortium. The SCC operates under the University of Kansas Center for Research’s non-profit 501(c)(3) U.S. tax status and received support from U.S. National Science Foundation grants from 1996 to 2018.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', + 'de-ch': + 'Specify wird vom Specify Collections Consortium (SCC) entwickelt, einer Kooperationsinitiative, die von ihren Mitgliedern geleitet und von institutionellen Partnern unterstützt wird. Die Softwareentwicklung und -unterstützung werden durch die Konsortiumsmitglieder ermöglicht, darunter das UniMus:Natur Consortium aus Norwegen, die Commonwealth Scientific and Industrial Research Organisation (CSIRO), der Consejo Superior de Investigaciones Científicas (CSIC), das Dänische Museumskonsortium, das Muséum d’Histoire Naturelle Genf, die University of Florida, die University of Kansas und die University of Michigan sowie zahlreiche weitere Sammlungen und Institutionen innerhalb des Konsortiums. Das SCC ist als gemeinnützige Organisation (501(c)(3)) des University of Kansas Center for Research anerkannt und wurde von 1996 bis 2018 von der U.S. National Science Foundation gefördert.', + 'es-es': + 'Specify es desarrollado por el Consorcio Specify Collections (SCC), una iniciativa colaborativa gobernada por sus miembros y respaldada por socios institucionales. El desarrollo y el soporte del software son posibles gracias a los miembros del consorcio, entre los que se incluyen el Consorcio UniMus:Natur de Noruega, la Organización de Investigación Científica e Industrial de la Commonwealth (CSIRO), el Consejo Superior de Investigaciones Científicas (CSIC), el Consorcio de Museos de Dinamarca, el Museo de Historia Natural de Ginebra, la Universidad de Florida, la Universidad de Kansas y la Universidad de Michigan, junto con numerosas otras colecciones e instituciones miembros del Consorcio. El SCC opera bajo la condición de organización sin fines de lucro 501(c)(3) del Centro de Investigación de la Universidad de Kansas y recibió apoyo de subvenciones de la Fundación Nacional de Ciencias de EE. UU. desde 1996 hasta 2018.', + 'fr-fr': + 'Specification est développé par le Consortium des Collections Specification (SCC), une initiative collaborative gérée par ses membres et soutenue par des partenaires institutionnels. Le développement et le support logiciel sont assurés par les membres du consortium, notamment le Consortium UniMus:Natur de Norvège, l’Organisation de recherche scientifique et industrielle du Commonwealth (CSIRO), le Conseil supérieur de la recherche scientifique (CSIC), le Consortium des musées du Danemark, le Muséum d’Histoire naturelle de Genève, l’Université de Floride, l’Université du Kansas et l’Université du Michigan, ainsi que de nombreuses autres collections et institutions membres du consortium. Le SCC fonctionne sous le statut d’organisme à but non lucratif (501(c)(3)) du Centre de recherche de l’Université du Kansas et a bénéficié de subventions de la Fondation nationale américaine pour la science (NSF) de 1996 à 2018.', 'hr-hr': - "Specify softver je proizvod konzorcija Specify Collections kojim upravljaju i financiraju ga njegove institucije članice. Osnivači konzorcija uključuju: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Danski konzorcij muzeja, Muséum d'Histoire Naturelle Geneva, Sveučilište Florida, Sveučilište Kansas i Sveučilište Michigan. Konzorcij djeluje pod neprofitnim, poreznim statusom 501(c)3 Sveučilišnog centra za istraživanje u Kansasu. Specify je od 1996. do 2018. godine financiran bespovratnim sredstvima američke Nacionalne zaklade za znanost.", - nb: '', + 'Specify je razvio Konzorcij za zbirke Specify (SCC), suradnička inicijativa kojom upravljaju njegovi članovi, a podržavaju je institucionalni partneri. Podršku i razvoj softvera omogućuju članovi konzorcija, uključujući UniMus:Natur konzorcij Norveške, Organizaciju za znanstvena i industrijska istraživanja Commonwealtha (CSIRO), Consejo Superior de Investigaciones Científicas (CSIC), Danski konzorcij muzeja, Muzej prirodne povijesti Ženeva, Sveučilište Florida, Sveučilište Kansas i Sveučilište Michigan, zajedno s brojnim drugim zbirkama i institucijama članicama Konzorcija. SCC djeluje pod neprofitnim poreznim statusom 501(c)(3) Sveučilišnog centra za istraživanje u Kansasu i primao je potporu od Nacionalne zaklade za znanost SAD-a od 1996. do 2018. godine.', + nb: 'Specifice er utviklet av Specifice Collections Consortium (SCC), et samarbeidsinitiativ styrt av medlemmene og støttet av institusjonelle partnere. Programvarestøtte og -utvikling er muliggjort av konsortiummedlemmer, inkludert UniMus:Natur Consortium of Norway, Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas (CSIC), Denmark Consortium of Museums, Muséum d’Histoire Naturelle Geneva, University of Florida, University of Kansas og University of Michigan, sammen med en rekke andre medlemssamlinger og institusjoner innenfor konsortiet. SCC opererer under University of Kansas Center for Researchs ideelle 501(c)(3) amerikanske skattestatus og mottok støtte fra amerikanske National Science Foundation-stipend fra 1996 til 2018.', 'pt-br': - "O software Specify é um produto do Specify Collections Consortium, que é governado e financiado por suas instituições membros. Os membros fundadores do Consórcio incluem: Commonwealth Scientific and Industrial Research Organisation (CSIRO), Consejo Superior de Investigaciones Científicas, Denmark Consortium of Museums, Muséum d'Histoire Naturelle Geneva, University of Florida, University of Kansas e University of Michigan. O Consórcio opera sob o status de organização sem fins lucrativos, 501(c)3, do Centro de Pesquisa da Universidade do Kansas, nos EUA. O Specify foi financiado de 1996 a 2018 por bolsas da Fundação Nacional de Ciência dos EUA (NSF).", - 'ru-ru': '', - 'uk-ua': '', + 'O Specify é desenvolvido pelo Specify Collections Consortium (SCC), uma iniciativa colaborativa governada por seus membros e apoiada por parceiros institucionais. O suporte e o desenvolvimento do software são viabilizados pelos membros do consórcio, incluindo o Consórcio UniMus:Natur da Noruega, a Organização de Pesquisa Científica e Industrial da Commonwealth (CSIRO), o Conselho Superior de Investigações Científicas (CSIC), o Consórcio de Museus da Dinamarca, o Museu de História Natural de Genebra, a Universidade da Flórida, a Universidade do Kansas e a Universidade de Michigan, juntamente com inúmeras outras coleções e instituições membros do Consórcio. O SCC opera sob o status de organização sem fins lucrativos 501(c)(3) do Centro de Pesquisa da Universidade do Kansas e recebeu apoio de bolsas da Fundação Nacional de Ciência dos EUA de 1996 a 2018.', + 'ru-ru': + 'Программа Specify разработана Консорциумом Specify Collections (SCC), совместной инициативой, управляемой его членами и поддерживаемой институциональными партнерами. Поддержка и разработка программного обеспечения стали возможны благодаря членам консорциума, включая консорциум UniMus:Natur из Норвегии, Организацию научных и промышленных исследований Содружества (CSIRO), Высший совет по научным исследованиям (CSIC), Датский консорциум музеев, Музей естественной истории Женевы, Университет Флориды, Университет Канзаса и Университет Мичигана, а также многочисленные другие коллекции и учреждения-члены консорциума. SCC работает в рамках некоммерческого налогового статуса 501(c)(3) Центра исследований Университета Канзаса и получал поддержку в виде грантов Национального научного фонда США с 1996 по 2018 год.', + 'uk-ua': + 'Specify розроблено Консорціумом колекцій Specify (SCC), спільною ініціативою, що керується його членами та підтримується інституційними партнерами. Підтримка та розробка програмного забезпечення здійснюється завдяки членам консорціуму, включаючи Консорціум UniMus:Natur Норвегії, Організацію наукових та промислових досліджень Співдружності (CSIRO), Вищу раду наукових досліджень (CSIC), Данський консорціум музеїв, Музей природної історії Женеви, Університет Флориди, Університет Канзасу та Університет Мічигану, а також численні інші колекції та установи-члени Консорціуму. SCC працює відповідно до некомерційного статусу Центру досліджень Канзаського університету, що підпадає під дію 501(c)(3) податкового кодексу США, та отримував підтримку від грантів Національного наукового фонду США з 1996 по 2018 рік.', }, licence: { 'en-us': 'Specify 7, Copyright 2026, University of Kansas Center for Research. Specify comes with ABSOLUTELY NO WARRANTY. This is free, open-source software licensed under GNU General Public License v3.', - 'de-ch': '', - 'es-es': '', - 'fr-fr': '', - 'hr-hr': '', - nb: '', - 'pt-br': '', - 'ru-ru': '', - 'uk-ua': '', + 'de-ch': + 'Specify 7, Copyright 2026, University of Kansas Center for Research. Specify wird OHNE JEGLICHE GEWÄHRLEISTUNG bereitgestellt. Dies ist freie Open-Source-Software, lizenziert unter der GNU General Public License v3.', + 'es-es': + 'Specify 7, Copyright 2026, Centro de Investigación de la Universidad de Kansas. Specify se distribuye SIN GARANTÍA ALGUNA. Este es un software libre de código abierto, licenciado bajo la Licencia Pública General GNU v3.', + 'fr-fr': + "Specify 7, © 2026, Centre de recherche de l'Université du Kansas. Specify est fourni SANS AUCUNE GARANTIE. Il s'agit d'un logiciel libre et open source distribué sous licence GNU GPL v3.", + 'hr-hr': + 'Specify 7, Autorsko pravo 2026., Istraživački centar Sveučilišta u Kansasu. Specify dolazi APSOLUTNO BEZ JAMSTVA. Ovo je besplatni softver otvorenog koda licenciran pod GNU General Public License v3.', + nb: 'Specificate 7, Copyright 2026, University of Kansas Center for Research. Specifice leveres UTEN GARANTI. Dette er gratis programvare med åpen kildekode lisensiert under GNU General Public License v3.', + 'pt-br': + 'Specify 7, Copyright 2026, Centro de Pesquisa da Universidade do Kansas. Specify é fornecido SEM QUALQUER GARANTIA. Este é um software livre e de código aberto licenciado sob a Licença Pública Geral GNU v3.', + 'ru-ru': + 'Specify 7, Copyright 2026, Центр исследований Канзасского университета. Программа Specify предоставляется БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ. Это бесплатное программное обеспечение с открытым исходным кодом, распространяемое по лицензии GNU General Public License v3.', + 'uk-ua': + 'Specify 7, авторське право 2026, Центр досліджень Канзаського університету. Specify постачається АБСОЛЮТНО БЕЗ ГАРАНТІЇ. Це безкоштовне програмне забезпечення з відкритим кодом, ліцензоване за Загальною публічною ліцензією GNU версії 3.', }, systemInformation: { 'en-us': 'System Information', From c7950eeb2b53fa4f3079bf1520f70341655d668e Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 10 Jul 2026 12:54:47 -0500 Subject: [PATCH 081/126] [test]: Backend unit test for Like operator (basic) --- .../backend/stored_queries/tests/test_query_ops.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 specifyweb/backend/stored_queries/tests/test_query_ops.py diff --git a/specifyweb/backend/stored_queries/tests/test_query_ops.py b/specifyweb/backend/stored_queries/tests/test_query_ops.py new file mode 100644 index 00000000000..35f7af0c9a1 --- /dev/null +++ b/specifyweb/backend/stored_queries/tests/test_query_ops.py @@ -0,0 +1,13 @@ +from unittest import TestCase +from sqlalchemy import column +from specifyweb.backend.stored_queries.query_ops import QueryOps + +class TestQueryOps(TestCase): + + def setUp(self): + self.ops = QueryOps(uiformatter=None) # clearing the formatting + + def test_op_like_basic(self): + result = self.ops.op_like(column("catalogNumber"), "%test%") # returns a SQLAlchemy object + sql = str(result.compile(compile_kwargs={"literal_binds": True})) # converts the whole thing into string + self.assertEqual(sql, '"catalogNumber" LIKE \'%test%\'') \ No newline at end of file From 4ed2d1d811a8d14cdb1cbd954ddd3fbfee6b0229 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 10 Jul 2026 13:00:55 -0500 Subject: [PATCH 082/126] [test]: Backend unit test for Like operator (percent wildcard) --- specifyweb/backend/stored_queries/tests/test_query_ops.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/specifyweb/backend/stored_queries/tests/test_query_ops.py b/specifyweb/backend/stored_queries/tests/test_query_ops.py index 35f7af0c9a1..0754911bbfa 100644 --- a/specifyweb/backend/stored_queries/tests/test_query_ops.py +++ b/specifyweb/backend/stored_queries/tests/test_query_ops.py @@ -10,4 +10,9 @@ def setUp(self): def test_op_like_basic(self): result = self.ops.op_like(column("catalogNumber"), "%test%") # returns a SQLAlchemy object sql = str(result.compile(compile_kwargs={"literal_binds": True})) # converts the whole thing into string - self.assertEqual(sql, '"catalogNumber" LIKE \'%test%\'') \ No newline at end of file + self.assertEqual(sql, '"catalogNumber" LIKE \'%test%\'') + + def test_op_like_percent_wildcard(self): + result = self.ops.op_like(column("catalogNumber"), "2025%") + sql = str(result.compile(compile_kwargs={"literal_binds": True})) + self.assertEqual(sql, '"catalogNumber" LIKE \'2025%\'') From 4ff4ab55de0b5a4e5e9c1f38b115bb78250cc682 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 10 Jul 2026 13:01:26 -0500 Subject: [PATCH 083/126] [test]: Backend unit test for Like operator (underscore wildcard) --- specifyweb/backend/stored_queries/tests/test_query_ops.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specifyweb/backend/stored_queries/tests/test_query_ops.py b/specifyweb/backend/stored_queries/tests/test_query_ops.py index 0754911bbfa..35d36cd95ba 100644 --- a/specifyweb/backend/stored_queries/tests/test_query_ops.py +++ b/specifyweb/backend/stored_queries/tests/test_query_ops.py @@ -16,3 +16,8 @@ def test_op_like_percent_wildcard(self): result = self.ops.op_like(column("catalogNumber"), "2025%") sql = str(result.compile(compile_kwargs={"literal_binds": True})) self.assertEqual(sql, '"catalogNumber" LIKE \'2025%\'') + + def test_op_like_underscore_wildcard(self): + result = self.ops.op_like(column("catalogNumber"), "202_") + sql = str(result.compile(compile_kwargs={"literal_binds": True})) + self.assertEqual(sql, '"catalogNumber" LIKE \'202_\'') From 97659cec8aa73eafc6f7b9639314a0e921e7054f Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 10 Jul 2026 13:03:22 -0500 Subject: [PATCH 084/126] [test]: Backend unit test for Like operator (no wildcard) --- specifyweb/backend/stored_queries/tests/test_query_ops.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specifyweb/backend/stored_queries/tests/test_query_ops.py b/specifyweb/backend/stored_queries/tests/test_query_ops.py index 35d36cd95ba..2cbf5579d25 100644 --- a/specifyweb/backend/stored_queries/tests/test_query_ops.py +++ b/specifyweb/backend/stored_queries/tests/test_query_ops.py @@ -21,3 +21,8 @@ def test_op_like_underscore_wildcard(self): result = self.ops.op_like(column("catalogNumber"), "202_") sql = str(result.compile(compile_kwargs={"literal_binds": True})) self.assertEqual(sql, '"catalogNumber" LIKE \'202_\'') + + def test_op_like_no_wildcard(self): + result = self.ops.op_like(column("catalogNumber"), "exact") + sql = str(result.compile(compile_kwargs={"literal_binds": True})) + self.assertEqual(sql, '"catalogNumber" LIKE \'exact\'') From db7eef5f00b2b6265ef1a3f17759c27dda2527cf Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Fri, 10 Jul 2026 13:15:33 -0500 Subject: [PATCH 085/126] [test]: reduced duplication by extracing a shared assertion --- .../stored_queries/tests/test_query_ops.py | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/specifyweb/backend/stored_queries/tests/test_query_ops.py b/specifyweb/backend/stored_queries/tests/test_query_ops.py index 2cbf5579d25..3a06561386d 100644 --- a/specifyweb/backend/stored_queries/tests/test_query_ops.py +++ b/specifyweb/backend/stored_queries/tests/test_query_ops.py @@ -5,24 +5,21 @@ class TestQueryOps(TestCase): def setUp(self): - self.ops = QueryOps(uiformatter=None) # clearing the formatting + self.ops = QueryOps(uiformatter=None) + + def assert_op_sql(self, op_method, value, expected_sql): + result = op_method(column("catalogNumber"), value) + sql = str(result.compile(compile_kwargs={"literal_binds": True})) + self.assertEqual(sql, expected_sql) def test_op_like_basic(self): - result = self.ops.op_like(column("catalogNumber"), "%test%") # returns a SQLAlchemy object - sql = str(result.compile(compile_kwargs={"literal_binds": True})) # converts the whole thing into string - self.assertEqual(sql, '"catalogNumber" LIKE \'%test%\'') - + self.assert_op_sql(self.ops.op_like, "%test%", '"catalogNumber" LIKE \'%test%\'') + def test_op_like_percent_wildcard(self): - result = self.ops.op_like(column("catalogNumber"), "2025%") - sql = str(result.compile(compile_kwargs={"literal_binds": True})) - self.assertEqual(sql, '"catalogNumber" LIKE \'2025%\'') + self.assert_op_sql(self.ops.op_like, "2025%", '"catalogNumber" LIKE \'2025%\'') def test_op_like_underscore_wildcard(self): - result = self.ops.op_like(column("catalogNumber"), "202_") - sql = str(result.compile(compile_kwargs={"literal_binds": True})) - self.assertEqual(sql, '"catalogNumber" LIKE \'202_\'') + self.assert_op_sql(self.ops.op_like, "202_", '"catalogNumber" LIKE \'202_\'') def test_op_like_no_wildcard(self): - result = self.ops.op_like(column("catalogNumber"), "exact") - sql = str(result.compile(compile_kwargs={"literal_binds": True})) - self.assertEqual(sql, '"catalogNumber" LIKE \'exact\'') + self.assert_op_sql(self.ops.op_like, "exact", '"catalogNumber" LIKE \'exact\'') \ No newline at end of file From f6cde74069c9ffa50dd03a5f7f1a225fd41ba994 Mon Sep 17 00:00:00 2001 From: Google Translate Date: Fri, 10 Jul 2026 18:03:43 +0000 Subject: [PATCH 086/126] Sync localization strings with Weblate Triggered by bdea3b27e5ed113b521b4405a06230c2782b7f12 on branch refs/heads/weblate-localization --- specifyweb/frontend/js_src/lib/localization/query.ts | 4 ++-- specifyweb/frontend/js_src/lib/localization/wbPlan.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts index de64debcd30..f5a6f8bc059 100644 --- a/specifyweb/frontend/js_src/lib/localization/query.ts +++ b/specifyweb/frontend/js_src/lib/localization/query.ts @@ -556,11 +556,11 @@ export const queryText = createDictionary({ 'en-us': 'Use "%" to match any number of characters.\n\nUse "_" to match a single character', 'ru-ru': - 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', + 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', 'es-es': 'Usar "%" para hacer coincidir cualquier número de caracteres.\n\nUsar "_" para hacer coincidir un solo carácter', 'fr-fr': - 'Utilisez « % » pour correspondre à un nombre quelconque de caractères.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nUtilisez « _ » pour correspondre à un seul caractère', + 'Utilisez « % » pour correspondre à un nombre quelconque de caractères.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nUtilisez « _ » pour correspondre à un seul caractère', 'uk-ua': 'Використовуйте "%", щоб відповідати будь-якій кількості символів.\n\nВикористовуйте "_", щоб відповідати одному символу', 'de-ch': diff --git a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts index af258de00e2..9af24812817 100644 --- a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts +++ b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts @@ -569,7 +569,7 @@ export const wbPlanText = createDictionary({ 'es-es': 'Está viendo las asignaciones de campos/mapeo para un conjunto de datos ya cargado.\n\nPara editar los mapeos, d´s marcha-atrás para los datos cargados o cree un nuevo conjunto de datos', 'fr-fr': - "Vous visualisez les correspondances d'un jeu de données importé.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPour modifier les correspondances, annulez l'importation des données ou créez un nouveau jeu de données.", + "Vous visualisez les correspondances d'un jeu de données importé.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPour modifier les correspondances, annulez l'importation des données ou créez un nouveau jeu de données.", 'uk-ua': 'Ви переглядаєте зіставлення для завантаженого набору даних.\n\nЩоб редагувати зіставлення, відкотіть завантажені дані або створіть новий набір даних', 'de-ch': From a2660b20c7bfcd32dc28171c6a5912ac566e5fef Mon Sep 17 00:00:00 2001 From: Google Translate Date: Fri, 10 Jul 2026 18:05:38 +0000 Subject: [PATCH 087/126] Sync localization strings with Weblate Triggered by 5ba9b2583443e6d772cddafaeedfd21bf8ed1353 on branch refs/heads/weblate-localization --- specifyweb/frontend/js_src/lib/localization/interactions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/interactions.ts b/specifyweb/frontend/js_src/lib/localization/interactions.ts index e47cecd8c50..98e9098501e 100644 --- a/specifyweb/frontend/js_src/lib/localization/interactions.ts +++ b/specifyweb/frontend/js_src/lib/localization/interactions.ts @@ -182,7 +182,7 @@ export const interactionsText = createDictionary({ 'fr-fr': 'Ajouter un objet non associé', 'uk-ua': "Додати непов'язаний елемент", 'de-ch': 'Nicht assoziierter Gegenstand hinzufügen', - 'pt-br': 'Adicionar item não associado', + 'pt-br': 'Adicionar item não relacionado', 'hr-hr': 'Dodaj nepovezanu stavku', nb: 'Legg til et ikke-tilknyttet objekt', }, @@ -204,7 +204,7 @@ export const interactionsText = createDictionary({ 'fr-fr': 'Les preparations ne peuvent être renvoyées dans ce contexte.', 'uk-ua': 'У цьому контексті препарати не можна повернути.', 'de-ch': 'Präparate können in diesem Kontext nicht zurückgegeben werden.', - 'pt-br': 'Neste contexto, os produtos não podem ser devolvidos.', + 'pt-br': 'Neste contexto, as preparações não podem ser devolvidas.', 'hr-hr': 'Pripreme se u ovom kontekstu ne mogu vratiti.', nb: 'Preparater kan ikke returneres i denne sammenhengen.', }, From 6bf77fea12ee9a09b1caffe6d0fbb4023517f5c2 Mon Sep 17 00:00:00 2001 From: Google Translate Date: Fri, 10 Jul 2026 18:11:46 +0000 Subject: [PATCH 088/126] Sync localization strings with Weblate Triggered by 07abe361b42fa36b31994d7768692ca4b01e8c9a on branch refs/heads/weblate-localization --- specifyweb/frontend/js_src/lib/localization/query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts index f5a6f8bc059..dbe9fa0b096 100644 --- a/specifyweb/frontend/js_src/lib/localization/query.ts +++ b/specifyweb/frontend/js_src/lib/localization/query.ts @@ -556,7 +556,7 @@ export const queryText = createDictionary({ 'en-us': 'Use "%" to match any number of characters.\n\nUse "_" to match a single character', 'ru-ru': - 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', + 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', 'es-es': 'Usar "%" para hacer coincidir cualquier número de caracteres.\n\nUsar "_" para hacer coincidir un solo carácter', 'fr-fr': From a9ad80bf00ef9ef8808ea9cda5a03aad9ebaf5de Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 09:23:42 -0500 Subject: [PATCH 089/126] test: check for containers that are no longer running --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2c81b911e0d..bb5afec1f55 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -97,7 +97,7 @@ jobs: docker logs mariadb echo "=================================================================================" docker logs specify7 - docker ps + docker ps --all - name: Upload report as artifact uses: actions/upload-artifact@v7 From 27abc3b026eca3466dbd790a95665e3b7fee72b5 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 09:49:47 -0500 Subject: [PATCH 090/126] test: try to stat mariadb tempfiles --- .github/workflows/coverage.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bb5afec1f55..2783e57727e 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -93,11 +93,13 @@ jobs: - name: Run the script id: run-coverage run: | - docker exec mariadb mariadb -uroot -ppassword -e 'select * from mysql.user where User = 'root';' || echo Mariadb crashed! + docker exec mariadb ls -lah /run docker logs mariadb echo "=================================================================================" - docker logs specify7 + docker exec mariadb ls -lah /run docker ps --all + docker exec mariadb grep '^#' /usr/lib/tmpfiles.d/mariadb.conf + - name: Upload report as artifact uses: actions/upload-artifact@v7 From c410291d71d71848279eb170ec5cb6ec829a9f2f Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 10:07:29 -0500 Subject: [PATCH 091/126] test: cat the entire file to see if it is commented or not --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2783e57727e..5d81cb0eec6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -98,7 +98,7 @@ jobs: echo "=================================================================================" docker exec mariadb ls -lah /run docker ps --all - docker exec mariadb grep '^#' /usr/lib/tmpfiles.d/mariadb.conf + docker exec mariadb cat /usr/lib/tmpfiles.d/mariadb.conf - name: Upload report as artifact From a1660c84ac4da740072fc677566f4c47caca9baa Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 10:20:04 -0500 Subject: [PATCH 092/126] test: check if mariadb is just slow, and also check out if the socket exists --- .github/workflows/coverage.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5d81cb0eec6..a406cc280ad 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -93,12 +93,10 @@ jobs: - name: Run the script id: run-coverage run: | - docker exec mariadb ls -lah /run + sleep 100 # so we can see if mariadb recovers docker logs mariadb echo "=================================================================================" - docker exec mariadb ls -lah /run - docker ps --all - docker exec mariadb cat /usr/lib/tmpfiles.d/mariadb.conf + docker exec mariadb ls -lah /run/mysqld - name: Upload report as artifact From 85234f32d90748ee78afe6335dc78e533266ce67 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 10:27:35 -0500 Subject: [PATCH 093/126] test: we know mariadb dies now, so lets find out why --- .github/workflows/coverage.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a406cc280ad..15060ad6191 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -96,7 +96,8 @@ jobs: sleep 100 # so we can see if mariadb recovers docker logs mariadb echo "=================================================================================" - docker exec mariadb ls -lah /run/mysqld + docker ps --all + docker exec mariadb ls -lah /run/mysqld || echo mariadb crashed! - name: Upload report as artifact From 27b00bed7026dcf5972f3fd929ec469bf3ffe2c3 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 11:01:06 -0500 Subject: [PATCH 094/126] test: check logs of specify7 to see if that is what is trying to create root again --- .github/workflows/coverage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 15060ad6191..bfdf3086617 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -89,6 +89,7 @@ jobs: done docker compose up --build -d docker stats --no-stream + docker logs specify7 - name: Run the script id: run-coverage From 619364e95f2d9c0778b34b69843e17894879faf6 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 11:07:05 -0500 Subject: [PATCH 095/126] fix: check logs of specify7 to see if that is what is trying to create root again --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bfdf3086617..3e5aeb286bc 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -89,7 +89,6 @@ jobs: done docker compose up --build -d docker stats --no-stream - docker logs specify7 - name: Run the script id: run-coverage @@ -99,6 +98,7 @@ jobs: echo "=================================================================================" docker ps --all docker exec mariadb ls -lah /run/mysqld || echo mariadb crashed! + docker logs specify7 - name: Upload report as artifact From c37adcecd861b55e6639708d3f6f9b28bf08cb3e Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 12:23:06 -0500 Subject: [PATCH 096/126] test: enable verbose logging for mariadb --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bc94997ef05..2a36e57dbba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: - "./seed-database:/docker-entrypoint-initdb.d" ports: - "127.0.0.1:3306:3306" # map host port 3306 to container port 3306 - command: --max_allowed_packet=1073741824 + command: --max_allowed_packet=1073741824 --general_log environment: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE=${DATABASE_NAME} From a0becac92da235fce78657a1538e37e5b5f5a5ef Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Mon, 13 Jul 2026 12:37:25 -0500 Subject: [PATCH 097/126] test: view all logs --- .github/workflows/coverage.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3e5aeb286bc..7c290e9609c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -94,11 +94,9 @@ jobs: id: run-coverage run: | sleep 100 # so we can see if mariadb recovers - docker logs mariadb - echo "=================================================================================" + docker compose logs docker ps --all docker exec mariadb ls -lah /run/mysqld || echo mariadb crashed! - docker logs specify7 - name: Upload report as artifact From 978562502e7f65c09f970b3100fd005e974638f2 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Tue, 14 Jul 2026 09:10:57 -0500 Subject: [PATCH 098/126] fix: restart mariadb then triage user creation --- .github/workflows/coverage.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7c290e9609c..f7ec3470618 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -88,15 +88,22 @@ jobs: sudo chmod -R a+rw . done docker compose up --build -d - docker stats --no-stream + + - name: Fix mariadb + id: reboot-mariadb + run: | + docker compose up -d mariadb + docker exec mariadb mariadb -uroot -ppassword -e"DROP USER root; + CREATE USER 'root'@'%' IDENTIFIED BY 'password'; + GRANT ALL ON *.* TO 'root'@'%' IDENTIFIED BY 'password'; + DROP DATABASE IF EXISTS specify;" + docker compose down + docker compose up + - name: Run the script id: run-coverage - run: | - sleep 100 # so we can see if mariadb recovers - docker compose logs - docker ps --all - docker exec mariadb ls -lah /run/mysqld || echo mariadb crashed! + run: run-coverage -t pj -a pj -d pj -o html,csv - name: Upload report as artifact From abc881aef0ffbec6f9d5b0891d0978d7e7de82e1 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Tue, 14 Jul 2026 09:17:12 -0500 Subject: [PATCH 099/126] fix: mariadb needs to crash before we restart it --- .github/workflows/coverage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f7ec3470618..0a530f54688 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -92,6 +92,8 @@ jobs: - name: Fix mariadb id: reboot-mariadb run: | + docker exec mariadb ls -lah /run/mysqld + sleep 30 docker compose up -d mariadb docker exec mariadb mariadb -uroot -ppassword -e"DROP USER root; CREATE USER 'root'@'%' IDENTIFIED BY 'password'; From c2101965d64fd6e05755cf40894c257a3b2d3ec5 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Tue, 14 Jul 2026 09:24:14 -0500 Subject: [PATCH 100/126] test: try to find mysqld.sock --- .github/workflows/coverage.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0a530f54688..86a27608436 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -92,9 +92,11 @@ jobs: - name: Fix mariadb id: reboot-mariadb run: | - docker exec mariadb ls -lah /run/mysqld - sleep 30 + sleep 10 docker compose up -d mariadb + sleep 10 + docker exec mariadb ls -lah /run/mysqld + docker logs mariadb docker exec mariadb mariadb -uroot -ppassword -e"DROP USER root; CREATE USER 'root'@'%' IDENTIFIED BY 'password'; GRANT ALL ON *.* TO 'root'@'%' IDENTIFIED BY 'password'; From 289cd252253d1ce62c296d95a1713c9c941ca074 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Tue, 14 Jul 2026 09:35:17 -0500 Subject: [PATCH 101/126] fix: detach from docker-compose and clean up run params --- .github/workflows/coverage.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 86a27608436..ab4ea752205 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -95,14 +95,13 @@ jobs: sleep 10 docker compose up -d mariadb sleep 10 - docker exec mariadb ls -lah /run/mysqld - docker logs mariadb docker exec mariadb mariadb -uroot -ppassword -e"DROP USER root; CREATE USER 'root'@'%' IDENTIFIED BY 'password'; GRANT ALL ON *.* TO 'root'@'%' IDENTIFIED BY 'password'; DROP DATABASE IF EXISTS specify;" docker compose down - docker compose up + docker compose up -d + sleep 10 - name: Run the script From 7870684365bcb0478a2fc6a1ee7a422947c4f51b Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Tue, 14 Jul 2026 12:03:34 -0500 Subject: [PATCH 102/126] fix: address CodeRabbit suggestions --- .github/workflows/README.md | 4 ++-- .github/workflows/coverage.yml | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 5bdddd51a1f..aa8fd99d3e8 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -73,10 +73,10 @@ Documentation for used GitHub secrets. COVERAGE_SCRIPT_CLONE Yes - Github Deploy token linked to the [development repo](github.com/specify/specify-development) to allow actions to clone it. + Github Deploy token linked to the [development repo](https://github.com/specify/specify-development) to allow actions to clone it. - + #8278 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ab4ea752205..1be51ff2f86 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,6 +1,7 @@ name: Coverage on: + pull_request: push: workflow_dispatch: @@ -15,8 +16,10 @@ jobs: # pull_requests, but only for external ones, so as not to run the action # twice if: | - github.event.head_commit.committer.name != 'Hosted Weblate' && - github.event.head_commit.committer.name != 'github-actions' && + (github.event_name == 'workflow_dispatch' || + (github.event.head_commit.committer.name != 'Hosted Weblate' && + github.event.head_commit.committer.name != 'github-actions')) + && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == true @@ -29,6 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 + with: persistent-credentials: false - name: Find all changed files uses: dorny/paths-filter@v3 @@ -53,7 +57,7 @@ jobs: needs: changes if: | (needs.changes.outputs.backend_changed || - needs.changes.outputs.frontend_changed) || + needs.changes.outputs.frontend) || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest @@ -66,6 +70,7 @@ jobs: sudo apt install docker-compose-v2 openssh-client - uses: actions/checkout@v4 + with: persistent-credentials: false - name: Fetch coverage script id: clone-coverage From e07d47f52586e8693f255c3614f93a021cdbd8e8 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 6 Jul 2026 10:38:39 -0500 Subject: [PATCH 103/126] [test]: verify needsSaved propagates from independent collection to parent --- .../DataModel/__tests__/resourceApi.test.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 9b4fda0164d..022c7574309 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -469,6 +469,51 @@ test('save', async () => { expect(newDetermination.get('number1')).toBe(2); }); +describe('independent resource change propagation', () => { + overrideAjax( + '/api/specify/collectionobject/?domainfilter=false&accession=11&offset=0', + emptyCollection + ); + overrideAjax(accessionUrl, accessionResponse, { method: 'PUT' }); + + test('needsSaved propagates from independent collection to parent', async () => { + const parentResource = new tables.Accession.Resource({ id: accessionId }); + expect(parentResource.needsSaved).toBe(false); + + const collectionObjectRel = + tables.CollectionObject.strictGetRelationship('accession')!; + + const independentCollection = + new tables.CollectionObject.IndependentCollection({ + related: parentResource, + field: collectionObjectRel, + }) as Collection; + + await independentCollection.fetch(); + + // Connect the collection to the parent's event system, as rgetCollection would do internally. + // storeIndependent is not in the public TS types so cast to any. + (parentResource as any).storeIndependent( + collectionObjectRel.getReverse(), + independentCollection + ); + + const newCollectionObject = new tables.CollectionObject.Resource({ + id: 998, + }); + independentCollection.add(newCollectionObject); + + // Adding an existing resource to an independent collection does not mark the resource itself needsSaved; + // only the parent is notified via saverequired + expect(newCollectionObject.needsSaved).toBe(false); + expect(parentResource.needsSaved).toBe(true); + + await parentResource.save(); + + expect(parentResource.needsSaved).toBe(false); + }); +}); + describe('resource initialization', () => { test('Initialization with dependent resources does not trigger saveRequired', () => { const resource = new tables.CollectionObject.Resource({ From 2614467a5df46690f9804c562801df98a7c10259 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 6 Jul 2026 10:43:54 -0500 Subject: [PATCH 104/126] [test]: verify independent resource field changes propagate needsSaved and persist after save --- .../DataModel/__tests__/resourceApi.test.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 022c7574309..573c805e1b0 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -514,6 +514,52 @@ describe('independent resource change propagation', () => { }); }); +describe('save base record which has independent subviews', () => { + overrideAjax( + '/api/specify/collectionobject/?domainfilter=false&accession=11&offset=0', + { + objects: [collectionObjectResponse], + meta: { limit: 20, offset: 0, total_count: 1 }, + } + ); + overrideAjax(accessionUrl, accessionResponse, { method: 'PUT' }); + + async function setupParentWithIndependentCollection() { + const parentResource = new tables.Accession.Resource({ id: accessionId }); + const collectionObjectRel = + tables.CollectionObject.strictGetRelationship('accession')!; + const independentCollection = + new tables.CollectionObject.IndependentCollection({ + related: parentResource, + field: collectionObjectRel, + }) as Collection; + await independentCollection.fetch(); + (parentResource as any).storeIndependent( + collectionObjectRel.getReverse(), + independentCollection + ); + return { parentResource, independentCollection }; + } + + test('modifying a field on an independent resource marks base record as needsSaved and save applies the change', async () => { + const { parentResource, independentCollection } = + await setupParentWithIndependentCollection(); + + expect(parentResource.needsSaved).toBe(false); + + const existingCollectionObject = independentCollection.models[0]; + existingCollectionObject.set('text1', 'changed-value'); + + expect(parentResource.needsSaved).toBe(true); + + await parentResource.save(); + + expect(parentResource.needsSaved).toBe(false); + // Change is preserved in memory after save + expect(existingCollectionObject.get('text1')).toBe('changed-value'); + }); +}); + describe('resource initialization', () => { test('Initialization with dependent resources does not trigger saveRequired', () => { const resource = new tables.CollectionObject.Resource({ From 37330a14d81ac4f65d9ffd63248a00f031371263 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 6 Jul 2026 10:58:40 -0500 Subject: [PATCH 105/126] verify independent subview sub-record changes propagate needsSaved and persist --- .../DataModel/__tests__/resourceApi.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts index 573c805e1b0..9fea1b20136 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/__tests__/resourceApi.test.ts @@ -558,6 +558,34 @@ describe('save base record which has independent subviews', () => { // Change is preserved in memory after save expect(existingCollectionObject.get('text1')).toBe('changed-value'); }); + + test('modifying a sub-record of an independent resource propagates needsSaved to base record and save completes', async () => { + const { parentResource, independentCollection } = + await setupParentWithIndependentCollection(); + + expect(parentResource.needsSaved).toBe(false); + + const collectionObject = independentCollection.models[0]; + + const determinations = + collectionObject.getDependentResource('determinations'); + + const determination = determinations!.models[0]; + + determination.set('number1', 99); + + // Change to sub-record propagates needsSaved all the way up to base record + + expect(parentResource.needsSaved).toBe(true); + + await parentResource.save(); + + expect(parentResource.needsSaved).toBe(false); + + // Change to sub-record is preserved in memory after save + + expect(determination.get('number1')).toBe(99); + }); }); describe('resource initialization', () => { From 351c0498b840ec98abd8ac426150260ff2aa4483 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 7 Jul 2026 16:00:00 -0500 Subject: [PATCH 106/126] created a permit object --- specifyweb/backend/businessrules/tests/test_permit.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 8d63ccc69b6..e4fd68bdc5c 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -95,6 +95,3 @@ def test_create_permit_with_agents(self): self.assertEqual(fetched.issuedby.firstname, 'Test') self.assertEqual(fetched.issuedto, new_issuedto) self.assertEqual(fetched.issuedto.firstname, 'Issued') - - - From 45528491b4483ea7f2f0e542376a1157e1c48801 Mon Sep 17 00:00:00 2001 From: Google Translate Date: Mon, 13 Jul 2026 16:03:01 +0000 Subject: [PATCH 107/126] Sync localization strings with Weblate Triggered by 57fd9d5aacde19705f0bed2ae12dcc7690018310 on branch refs/heads/weblate-localization --- specifyweb/frontend/js_src/lib/localization/query.ts | 4 ++-- specifyweb/frontend/js_src/lib/localization/wbPlan.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts index dbe9fa0b096..eae9989c928 100644 --- a/specifyweb/frontend/js_src/lib/localization/query.ts +++ b/specifyweb/frontend/js_src/lib/localization/query.ts @@ -556,11 +556,11 @@ export const queryText = createDictionary({ 'en-us': 'Use "%" to match any number of characters.\n\nUse "_" to match a single character', 'ru-ru': - 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', + 'Используйте символ "%" для сопоставления любого количества символов.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nИспользуйте символ "_" для сопоставления одного символа.', 'es-es': 'Usar "%" para hacer coincidir cualquier número de caracteres.\n\nUsar "_" para hacer coincidir un solo carácter', 'fr-fr': - 'Utilisez « % » pour correspondre à un nombre quelconque de caractères.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nUtilisez « _ » pour correspondre à un seul caractère', + 'Utilisez « % » pour correspondre à un nombre quelconque de caractères.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nUtilisez « _ » pour correspondre à un seul caractère', 'uk-ua': 'Використовуйте "%", щоб відповідати будь-якій кількості символів.\n\nВикористовуйте "_", щоб відповідати одному символу', 'de-ch': diff --git a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts index 9af24812817..41d705b5e9f 100644 --- a/specifyweb/frontend/js_src/lib/localization/wbPlan.ts +++ b/specifyweb/frontend/js_src/lib/localization/wbPlan.ts @@ -569,7 +569,7 @@ export const wbPlanText = createDictionary({ 'es-es': 'Está viendo las asignaciones de campos/mapeo para un conjunto de datos ya cargado.\n\nPara editar los mapeos, d´s marcha-atrás para los datos cargados o cree un nuevo conjunto de datos', 'fr-fr': - "Vous visualisez les correspondances d'un jeu de données importé.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPour modifier les correspondances, annulez l'importation des données ou créez un nouveau jeu de données.", + "Vous visualisez les correspondances d'un jeu de données importé.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nPour modifier les correspondances, annulez l'importation des données ou créez un nouveau jeu de données.", 'uk-ua': 'Ви переглядаєте зіставлення для завантаженого набору даних.\n\nЩоб редагувати зіставлення, відкотіть завантажені дані або створіть новий набір даних', 'de-ch': From 6fa4a2c10c2e811c7f2dcde44d80d3c4b1762219 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 13 Jul 2026 16:36:55 -0500 Subject: [PATCH 108/126] [test]: Backend unit test for Timestamp Created in query results --- .../tests/test_views/test_timestamp.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py diff --git a/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py b/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py new file mode 100644 index 00000000000..6306f6f48c8 --- /dev/null +++ b/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py @@ -0,0 +1,48 @@ +from specifyweb.backend.stored_queries.tests.tests import SQLAlchemySetup +from .raw_query import get_simple_query +from django.test import Client +from unittest.mock import patch, Mock +import json + +class TestTimestampQuery(SQLAlchemySetup): + + @patch("specifyweb.backend.stored_queries.execution.models.session_context") + def test_timestamp_created_in_results(self, context: Mock): + context.return_value = TestTimestampQuery.test_session_context() + + c = Client() + c.force_login(self.specifyuser) + + query = get_simple_query(self.specifyuser).copy() + query["fields"] = query["fields"] + [ + { + "tablelist": "1", + "stringid": "1.collectionobject.timestampCreated", + "fieldname": "timestampCreated", + "isrelfld": False, + "sorttype": 0, + "position": 1, + "isdisplay": True, + "operstart": 8, + "startvalue": "", + "isnot": False, + "isstrict": False, + "_tableName": "SpQueryField" + } + ] + + response = c.post(f'/stored_query/ephemeral/', query, content_type="application/json") + self._assertStatusCodeEqual(response, 200) + + results = json.loads(response.content.decode())["results"] + + # Should have 5 results (5 collection objects from setUp) + self.assertEqual(len(results), 5) + + # Each result should be [id, catalogNumber, timestampCreated] + for i, row in enumerate(results): + self.assertEqual(row[0], self.collectionobjects[i].id) + self.assertEqual(row[1], f"num-{i}") + # Timestamp should not be None or empty + self.assertIsNotNone(row[2]) + self.assertNotEqual(row[2], "") \ No newline at end of file From 6be8aa8ee33792f263d10d3fd18c8258af5e787a Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 13 Jul 2026 16:45:56 -0500 Subject: [PATCH 109/126] [test]: Add timestampModified coverage to query results test --- .../tests/test_views/test_timestamp.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py b/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py index 6306f6f48c8..03dbb98ccbf 100644 --- a/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py +++ b/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py @@ -28,6 +28,20 @@ def test_timestamp_created_in_results(self, context: Mock): "isnot": False, "isstrict": False, "_tableName": "SpQueryField" + }, + { + "tablelist": "1", + "stringid": "1.collectionobject.timestampModified", + "fieldname": "timestampModified", + "isrelfld": False, + "sorttype": 0, + "position": 2, + "isdisplay": True, + "operstart": 8, + "startvalue": "", + "isnot": False, + "isstrict": False, + "_tableName": "SpQueryField" } ] @@ -39,10 +53,13 @@ def test_timestamp_created_in_results(self, context: Mock): # Should have 5 results (5 collection objects from setUp) self.assertEqual(len(results), 5) - # Each result should be [id, catalogNumber, timestampCreated] + # Each result should be [id, catalogNumber, timestampCreated, timestampModified] for i, row in enumerate(results): self.assertEqual(row[0], self.collectionobjects[i].id) self.assertEqual(row[1], f"num-{i}") - # Timestamp should not be None or empty + # Timestamp Created should not be None or empty self.assertIsNotNone(row[2]) - self.assertNotEqual(row[2], "") \ No newline at end of file + self.assertNotEqual(row[2], "") + # Timestamp Modified should not be None or empty + self.assertIsNotNone(row[3]) + self.assertNotEqual(row[3], "") \ No newline at end of file From ecf356d7fafd1a803e18ae1989e3bafb5f97b0c3 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 13 Jul 2026 16:46:42 -0500 Subject: [PATCH 110/126] [fix]: Removed unnecessary f string --- .../backend/stored_queries/tests/test_views/test_timestamp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py b/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py index 03dbb98ccbf..cbfb3a94b2a 100644 --- a/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py +++ b/specifyweb/backend/stored_queries/tests/test_views/test_timestamp.py @@ -45,7 +45,7 @@ def test_timestamp_created_in_results(self, context: Mock): } ] - response = c.post(f'/stored_query/ephemeral/', query, content_type="application/json") + response = c.post('/stored_query/ephemeral/', query, content_type="application/json") self._assertStatusCodeEqual(response, 200) results = json.loads(response.content.decode())["results"] From 7abb6f4d89f49113bd76cd4c60ff80ee5046aeb3 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 14:23:25 -0500 Subject: [PATCH 111/126] [test]: create a permit first --- specifyweb/backend/businessrules/tests/test_permit.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index e4fd68bdc5c..58bd76ad386 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -95,3 +95,11 @@ def test_create_permit_with_agents(self): self.assertEqual(fetched.issuedby.firstname, 'Test') self.assertEqual(fetched.issuedto, new_issuedto) self.assertEqual(fetched.issuedto.firstname, 'Issued') + + + def test_add_and_delete_attachment(self): + # Create a permit first + permit = models.Permit.objects.create( + institution=self.institution, + permitnumber='P-ATT-001', + ) From 5b7874f25bc19158511aded617418b8c5be378b4 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 14:24:42 -0500 Subject: [PATCH 112/126] [test]: creating an attachment --- .../backend/businessrules/tests/test_permit.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 58bd76ad386..65d944991d2 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -103,3 +103,16 @@ def test_add_and_delete_attachment(self): institution=self.institution, permitnumber='P-ATT-001', ) + + # Create an attachment + attachment = models.Attachment.objects.create( + origfilename='permit_doc.pdf', + tableid=permit.specify_model.tableId, + title='Field Permit', + ) + + permit_attachment = models.Permitattachment.objects.create( + permit=permit, + attachment=attachment, + ordinal=0, + ) From a683cb8c144c773528ec3e45f585317eb6fbeb5c Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 14:25:34 -0500 Subject: [PATCH 113/126] [test]: verifying attachment is linked --- specifyweb/backend/businessrules/tests/test_permit.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 65d944991d2..e2f31dc59f4 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -116,3 +116,11 @@ def test_add_and_delete_attachment(self): attachment=attachment, ordinal=0, ) + + # Verify attachment is linked + self.assertEqual(permit.permitattachments.count(), 1) + self.assertEqual( + permit.permitattachments.first().attachment.origfilename, + 'permit_doc.pdf' + ) + From 2a1d19ebaa795d01df8dca8d64f47f65ef873567 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 14:26:41 -0500 Subject: [PATCH 114/126] [test]: deleting the attachment --- specifyweb/backend/businessrules/tests/test_permit.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index e2f31dc59f4..795fab483c5 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -124,3 +124,8 @@ def test_add_and_delete_attachment(self): 'permit_doc.pdf' ) + # Delete the attachment + attachment_id = attachment.id + permit_attachment.delete() + attachment.delete() + From ace1b1b9560e16b807059f64dd03fcc282968941 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 14:27:17 -0500 Subject: [PATCH 115/126] [test]: verifying that it's gone --- specifyweb/backend/businessrules/tests/test_permit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 795fab483c5..38a0a426606 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -129,3 +129,7 @@ def test_add_and_delete_attachment(self): permit_attachment.delete() attachment.delete() + # Verifying it's gone + self.assertEqual(permit.permitattachments.count(), 0) + self.assertEqual(models.Attachment.objects.filter(id=attachment_id).count(), 0) + From 08e4230c5fdc854e0dbb9f7e314788bd594db582 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 14:47:36 -0500 Subject: [PATCH 116/126] [fix]: refetch from DB instead of using stale in-memory object --- .../businessrules/tests/test_permit.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 38a0a426606..ad4867875e0 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -106,28 +106,30 @@ def test_add_and_delete_attachment(self): # Create an attachment attachment = models.Attachment.objects.create( - origfilename='permit_doc.pdf', - tableid=permit.specify_model.tableId, - title='Field Permit', + origfilename='permit_doc.pdf', + tableid=permit.specify_model.tableId, + title='Field Permit', ) - permit_attachment = models.Permitattachment.objects.create( - permit=permit, - attachment=attachment, - ordinal=0, + permit=permit, + attachment=attachment, + ordinal=0, ) # Verify attachment is linked self.assertEqual(permit.permitattachments.count(), 1) self.assertEqual( - permit.permitattachments.first().attachment.origfilename, - 'permit_doc.pdf' + permit.permitattachments.first().attachment.origfilename, + 'permit_doc.pdf' ) # Delete the attachment attachment_id = attachment.id permit_attachment.delete() - attachment.delete() + # Re-fetch from DB instead of using stale in-memory object + attachment_to_delete = models.Attachment.objects.get(id=attachment_id) + attachment_to_delete.delete() + # Verifying it's gone self.assertEqual(permit.permitattachments.count(), 0) From 80757b351d096eb2f6be0bc5f34aa68d62f198a8 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Wed, 8 Jul 2026 14:51:26 -0500 Subject: [PATCH 117/126] [fix]: didn --- specifyweb/backend/businessrules/tests/test_permit.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index ad4867875e0..43a91327236 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -124,14 +124,9 @@ def test_add_and_delete_attachment(self): ) # Delete the attachment - attachment_id = attachment.id permit_attachment.delete() - # Re-fetch from DB instead of using stale in-memory object - attachment_to_delete = models.Attachment.objects.get(id=attachment_id) - attachment_to_delete.delete() - # Verifying it's gone self.assertEqual(permit.permitattachments.count(), 0) - self.assertEqual(models.Attachment.objects.filter(id=attachment_id).count(), 0) + self.assertEqual(models.Attachment.objects.filter(id=attachment.id).count(), 0) From 1708ce2209f1ad32b0192cd2fb41d9f12030aed8 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 13 Jul 2026 09:17:31 -0500 Subject: [PATCH 118/126] final changes --- .env | 6 +++--- specifyweb/backend/businessrules/tests/test_permit.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 71fe3b0c430..4c8e8aec296 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ DATABASE_HOST=mariadb DATABASE_PORT=3306 MYSQL_ROOT_PASSWORD=password -DATABASE_NAME=specify +DATABASE_NAME=Naturkundemuseum # The following are database users with specific roles and privileges. # If the migrator and app user are not defined, the system will use the master user credentials. @@ -24,8 +24,8 @@ MIGRATOR_HOST=% # APP Database User # Normal runtime database user that performs application-level operations. # Make sure that the user is unique to just one database, otherwise use master. -APP_USER_NAME=specify_user -APP_USER_PASSWORD=specify_user +APP_USER_NAME=spadmin +APP_USER_PASSWORD=testuser APP_HOST=% # Enabling this option allows administrators with access to the diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 43a91327236..ecd5651dfc8 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -123,9 +123,12 @@ def test_add_and_delete_attachment(self): 'permit_doc.pdf' ) - # Delete the attachment + # Delete the permit_attachment connector first permit_attachment.delete() + # Delete the attachment then after + + # Verifying it's gone self.assertEqual(permit.permitattachments.count(), 0) self.assertEqual(models.Attachment.objects.filter(id=attachment.id).count(), 0) From 3d0ac2d5f1cc79f41de21a10c692457ecead80d8 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 13 Jul 2026 13:51:24 -0500 Subject: [PATCH 119/126] [fix]: added comments for clarification on permit attachment deletion --- specifyweb/backend/businessrules/tests/test_permit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index ecd5651dfc8..4e49dcd0f1a 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -125,8 +125,9 @@ def test_add_and_delete_attachment(self): # Delete the permit_attachment connector first permit_attachment.delete() - - # Delete the attachment then after + # The Attachment is auto-deleted by a post_delete signal handler in + # attachment_rules.py (attachment_jointable_deletion). When a Permitattachment join row is deleted, + # the signal fires and calls obj.attachment.delete(). The test passes as-is. # Verifying it's gone From 0deabf51aabf22f561b767f3dd8f212643e06b00 Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Mon, 13 Jul 2026 14:13:41 -0500 Subject: [PATCH 120/126] [fix]: remove .env from branch --- .env | 81 ------------------------------------------------------------ 1 file changed, 81 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 4c8e8aec296..00000000000 --- a/.env +++ /dev/null @@ -1,81 +0,0 @@ -DATABASE_HOST=mariadb -DATABASE_PORT=3306 -MYSQL_ROOT_PASSWORD=password -DATABASE_NAME=Naturkundemuseum - -# The following are database users with specific roles and privileges. -# If the migrator and app user are not defined, the system will use the master user credentials. -# See documenation https://discourse.specifysoftware.org/t/new-blank-database-creation-database-user-levels/3023 - -# MASTER Database User -# Full database administrator, used for initial setup and migrations requiring elevated privileges. -# This user should already be setup before running Specify. -MASTER_NAME=root -MASTER_PASSWORD=password -MASTER_HOST=% - -# MIGRATOR Database User -# User with elevated privileges to perform migrations (create/drop/modify tables, etc.), for Django migration steps. -# Make sure that the user is unique to just one database, otherwise use master. -MIGRATOR_NAME=specify_migrator -MIGRATOR_PASSWORD=specify_migrator -MIGRATOR_HOST=% - -# APP Database User -# Normal runtime database user that performs application-level operations. -# Make sure that the user is unique to just one database, otherwise use master. -APP_USER_NAME=spadmin -APP_USER_PASSWORD=testuser -APP_HOST=% - -# Enabling this option allows administrators with access to the -# backend Specify instance to log in as any user for support -# purposes without knowing their password. -# https://discourse.specifysoftware.org/t/allow-support-login-documentation/2838 -ALLOW_SUPPORT_LOGIN=false -# The amount of time in seconds each token is valid for -SUPPORT_LOGIN_TTL = 180 - -# Make sure to set the `SECRET_KEY` to a unique value -SECRET_KEY=change_this_to_some_unique_random_string - -ASSET_SERVER_URL=http://host.docker.internal/web_asset_store.xml -# Make sure to set the `ASSET_SERVER_KEY` to a unique value -ASSET_SERVER_KEY=your_asset_server_access_key - -# Information to connect to a Redis database -# Specify will use this database as a process broker and storage for temporary -# values -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_DB_INDEX=0 - -REPORT_RUNNER_HOST=report-runner -REPORT_RUNNER_PORT=8080 - -CELERY_BROKER_URL=redis://redis/0 -CELERY_RESULT_BACKEND=redis://redis/1 - -# Local time zone for this installation. Choices can be found here: -# https://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = America/Chicago - -# This variable controls the Specify 7 logging level. Possible values -# are: -# * DEBUG: Low level system information for debugging purposes. -# * INFO: General system information. -# * WARNING: Information describing a minor problem that has occurred. -# * ERROR: Information describing a major problem that has occurred. -# * CRITICAL: Information describing a critical problem that has occurred. -LOG_LEVEL=WARNING - -# Set this variable to `true` to run Specify 7 in debug mode. This -# should only be used during development and troubleshooting and not -# during general use. Django applications leak memory when operated -# continuously in debug mode. -SP7_DEBUG=true From da4607c41b2dcdb052fa28f956a0820308ccd283 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 14 Jul 2026 16:15:37 +0000 Subject: [PATCH 121/126] Sync localization strings with Weblate Triggered by 4fd4e684f158b9045a974f3ca9efab070f7cfe55 on branch refs/heads/weblate-localization --- .../frontend/js_src/lib/localization/forms.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/forms.ts b/specifyweb/frontend/js_src/lib/localization/forms.ts index efa196bb426..416d9b86709 100644 --- a/specifyweb/frontend/js_src/lib/localization/forms.ts +++ b/specifyweb/frontend/js_src/lib/localization/forms.ts @@ -19,7 +19,7 @@ export const formsText = createDictionary({ 'de-ch': 'Formulare', 'pt-br': 'Formulários', 'hr-hr': 'Obrasci', - nb: 'Skjemaer', + nb: 'Skjema', }, clone: { 'en-us': 'Clone', @@ -30,7 +30,7 @@ export const formsText = createDictionary({ 'de-ch': 'Klone', 'pt-br': 'Clone', 'hr-hr': 'Klon', - nb: 'Klon', + nb: 'Klone', }, cloneDescription: { 'en-us': 'Create a full copy of current record', @@ -41,7 +41,7 @@ export const formsText = createDictionary({ 'de-ch': 'Erstellen einer kompletten Kopie des aktuellen Datensatzes', 'pt-br': 'Criar uma cópia completa do registro atual', 'hr-hr': 'Izradi potpunu kopiju trenutnog zapisa', - nb: 'Opprett en fullstendig kopi av gjeldende post', + nb: 'Opprett en full kopi av gjeldende post', }, valueMustBeUniqueToField: { 'en-us': 'Value must be unique to {fieldName:string}', @@ -109,7 +109,7 @@ export const formsText = createDictionary({ 'de-ch': 'Es wird geprüft, ob die Ressource gelöscht werden kann…', 'pt-br': 'Verificando se o recurso pode ser excluído…', 'hr-hr': 'Provjeravam može li se resurs izbrisati…', - nb: 'Sjekker om ressursen kan slettes…', + nb: 'Kontrollerer om ressursen kan slettes …', }, checkingIfResourceIsUsed: { 'en-us': 'Checking if this record is currently in use…', @@ -120,7 +120,7 @@ export const formsText = createDictionary({ 'ru-ru': 'Проверка, используется ли эта запись в данный момент…', 'uk-ua': 'Перевірка, чи цей запис зараз використовується…', 'hr-hr': 'Provjeravam je li ovaj zapis trenutno u upotrebi…', - nb: 'Sjekker om denne oppføringen er i bruk for øyeblikket…', + nb: 'Kontrollerer om denne posten er i bruk …', }, noLinkedRecords: { 'en-us': 'No linked records', @@ -144,7 +144,7 @@ export const formsText = createDictionary({ 'ru-ru': 'В настоящее время данный ресурс связан со следующими записями:', 'uk-ua': 'Цей ресурс наразі пов’язаний з такими записами:', 'hr-hr': 'Ovaj resurs je trenutno povezan sa sljedećim zapisima:', - nb: 'Denne ressursen er for øyeblikket koblet til følgende poster:', + nb: 'Denne ressursen er for øyeblikket knyttet til følgende poster:', }, deleteBlocked: { 'en-us': 'Delete blocked', @@ -155,7 +155,7 @@ export const formsText = createDictionary({ 'de-ch': 'Löschen blockiert', 'pt-br': 'Excluir bloqueado', 'hr-hr': 'Izbriši blokirano', - nb: 'Slett blokkert', + nb: 'Sletting blokkert', }, deleteBlockedDescription: { 'en-us': @@ -173,7 +173,7 @@ export const formsText = createDictionary({ 'O recurso não pode ser excluído porque é referenciado pelos seguintes recursos:', 'hr-hr': 'Resurs se ne može izbrisati jer se na njega pozivaju sljedeći resursi:', - nb: 'Ressursen kan ikke slettes fordi den refereres til av følgende ressurser:', + nb: 'Ressursen kan ikke slettes fordi den refereres av følgende ressurser:', }, relationship: { 'en-us': 'Relationship', @@ -184,7 +184,7 @@ export const formsText = createDictionary({ 'de-ch': 'Beziehung', 'pt-br': 'Relação', 'hr-hr': 'Odnos', - nb: 'Forhold', + nb: 'Relasjon', }, paleoMap: { 'en-us': 'Paleo Map', @@ -195,7 +195,7 @@ export const formsText = createDictionary({ 'de-ch': 'Paläokarte', 'pt-br': 'Mapa Paleo', 'hr-hr': 'Paleo karta', - nb: 'Paleo-kart', + nb: 'Paleokart', }, paleoRequiresGeography: { comment: 'Example: Geography Required', @@ -225,7 +225,7 @@ export const formsText = createDictionary({ 'O plugin Paleo Map requer que o {localityTable:string} tenha coordenadas geográficas e que o contexto paleo tenha uma idade geográfica com pelo menos um horário de início e um horário de término preenchidos.', 'hr-hr': 'Paleo Map dodatak zahtijeva da {localityTable:string} ima geografske koordinate i da paleo kontekst ima geografsku starost s barem popunjenim vremenom početka ili i vremenom završetka.', - nb: 'Paleo Map-pluginen krever at {localityTable:string} har geografiske koordinater og at paleo-konteksten har en geografisk alder med minst en starttid eller et sluttid utfylt.', + nb: 'Paleokart-programtillegget krever at tabellen {localityTable:string} har geografiske koordinater, og at paleokonteksten har en geografisk alder med minst et starttidspunkt eller et sluttidspunkt angitt.', }, invalidDate: { 'en-us': 'Invalid Date', @@ -291,7 +291,7 @@ export const formsText = createDictionary({ 'de-ch': 'Monat / Jahr', 'pt-br': 'Segunda-feira / Ano', 'hr-hr': 'Pon / God.', - nb: 'Man / År', + nb: 'Mnd / år', }, yearPlaceholder: { comment: @@ -326,7 +326,7 @@ export const formsText = createDictionary({ 'de-ch': 'Auf aktuelles Datum einstellen', 'pt-br': 'Definir para a data atual', 'hr-hr': 'Postavi na trenutni datum', - nb: 'Sett til gjeldende dato', + nb: 'Bruk dagens dato', }, addToPickListConfirmation: { 'en-us': 'Add to {pickListTable:string}?', @@ -425,7 +425,7 @@ export const formsText = createDictionary({ 'de-ch': 'Datensatz zuerst speichern', 'pt-br': 'Salvar registro primeiro', 'hr-hr': 'Prvo spremi zapis', - nb: 'Lagre oppføringen først', + nb: 'Lagre posten først', }, firstRecord: { 'en-us': 'First Record', From ead47e5ca47185b116f29112f26167008c5bf42e Mon Sep 17 00:00:00 2001 From: Rijul Poudel Date: Tue, 7 Jul 2026 16:00:00 -0500 Subject: [PATCH 122/126] created a permit object --- specifyweb/backend/businessrules/tests/test_permit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/specifyweb/backend/businessrules/tests/test_permit.py b/specifyweb/backend/businessrules/tests/test_permit.py index 4e49dcd0f1a..e4f362d141a 100644 --- a/specifyweb/backend/businessrules/tests/test_permit.py +++ b/specifyweb/backend/businessrules/tests/test_permit.py @@ -51,6 +51,7 @@ def test_create_permit_with_fields(self): text1='Filed under drawer 3', number1=42.5, ) + # Save + verify all fields persisted self.assertIsNotNone(permit.id) From 9b566c5363ba95449cd6aa49a916ddbdd4718928 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 15 Jul 2026 09:18:33 -0500 Subject: [PATCH 123/126] fix: remove debugging flag from mariadb container entrypoint --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2a36e57dbba..bc94997ef05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: - "./seed-database:/docker-entrypoint-initdb.d" ports: - "127.0.0.1:3306:3306" # map host port 3306 to container port 3306 - command: --max_allowed_packet=1073741824 --general_log + command: --max_allowed_packet=1073741824 environment: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE=${DATABASE_NAME} From bfc91ca49f2b56580a4abbc3b7f330ed1581faa5 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 15 Jul 2026 09:26:46 -0500 Subject: [PATCH 124/126] fix: re-create .env since that is in `.gitignore` now --- .github/workflows/coverage.yml | 43 +++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 1be51ff2f86..1d342cd8b7c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -85,7 +85,48 @@ jobs: - name: Configure specify id: config-specify run: | - sed -i '/DATABASE_NAME/!s/\(.*\)\(NAME=\).*/\1\2root/; s/\(.*\)\(PASSWORD=\).*/\1\2password/; s/\(.*\) = \(.*\)/\1=\2/' .env + cat > .env <<-EOS + DATABASE_HOST=mariadb + DATABASE_PORT=3306 + MYSQL_ROOT_PASSWORD=password + DATABASE_NAME=specify + + MASTER_NAME=root + MASTER_PASSWORD=password + MASTER_HOST=% + + MIGRATOR_NAME=root + MIGRATOR_PASSWORD=password + MIGRATOR_HOST=% + + APP_USER_NAME=root + APP_USER_PASSWORD=password + APP_HOST=% + + ALLOW_SUPPORT_LOGIN=false + SUPPORT_LOGIN_TTL=180 + + SECRET_KEY=change_this_to_some_unique_random_string + + ASSET_SERVER_URL=http://host.docker.internal/web_asset_store.xml + ASSET_SERVER_KEY=your_asset_server_access_key + REDIS_HOST=redis + REDIS_PORT=6379 + REDIS_DB_INDEX=0 + + REPORT_RUNNER_HOST=report-runner + REPORT_RUNNER_PORT=8080 + + CELERY_BROKER_URL=redis://redis/0 + CELERY_RESULT_BACKEND=redis://redis/1 + + TIME_ZONE=America/Chicago + + LOG_LEVEL=WARNING + + SP7_DEBUG=true + EOS + sed -i 's/#[ ]*\(.*requirements-testing.txt\)/\1/; s/\(COPY\) \(requirements-testing.txt\)/\1 --chown=specify:specify \2/' Dockerfile sed -i 's/\(\(DATABASE_\|MASTER_\|MIGRATOR\|APP_USER_\)[^O].*\) = \(.*\)/\1 = os.environ.get("\1", "")/' specifyweb/settings/specify_settings.py for i in range {0..3}; do From 07882a2ca02d9b32313512552a91e338ee690e84 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 15 Jul 2026 13:06:05 -0500 Subject: [PATCH 125/126] fix: fix yaml syntax --- .github/workflows/coverage.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 1d342cd8b7c..56ae6a8f9a7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -32,7 +32,8 @@ jobs: steps: - uses: actions/checkout@v4 - with: persistent-credentials: false + with: + persistent-credentials: false - name: Find all changed files uses: dorny/paths-filter@v3 @@ -70,7 +71,8 @@ jobs: sudo apt install docker-compose-v2 openssh-client - uses: actions/checkout@v4 - with: persistent-credentials: false + with: + persistent-credentials: false - name: Fetch coverage script id: clone-coverage From 16368d7459c3cc7e6dcd61ce4b9c85ccc1fb9c87 Mon Sep 17 00:00:00 2001 From: g1rly-c0d3r Date: Wed, 15 Jul 2026 13:07:57 -0500 Subject: [PATCH 126/126] fix: fix yaml syntax --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 56ae6a8f9a7..7b358ba6250 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - persistent-credentials: false + persist-credentials: false - name: Find all changed files uses: dorny/paths-filter@v3 @@ -72,7 +72,7 @@ jobs: - uses: actions/checkout@v4 with: - persistent-credentials: false + persist-credentials: false - name: Fetch coverage script id: clone-coverage