diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5c051880f6..ba4908c216 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -24,7 +24,7 @@ A clear and concise description of what you expected to happen. ### Context / Kontext -Please state your operating system, the RDMO version, and (if applicable) the browser the error occured in. +Please state your operating system, the RDMO version, and (if applicable) the browser the error occurred in. ### References / Verweise diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2b16cc48e3..81722d962a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,14 +1,24 @@ - + + - + + + + + + + + + + + + + ## Description - - - - + Related issue: #ISSUE_NUMBER ## Motivation and Context @@ -21,27 +31,6 @@ Related issue: #ISSUE_NUMBER ## Screenshots (if appropriate) -## Types of Changes - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to change) -- [ ] Code style update (formatting, renaming) -- [ ] Refactoring (no functional changes, no api changes) -- [ ] Build related changes -- [ ] Documentation content changes -- [ ] Other (please describe): - -## Checklist - - -- [ ] I have read the [contributor guide](https://github.com/rdmorganiser/rdmo/blob/main/CONTRIBUTING.md). -- [ ] My code follows the code style of this project. -- [ ] My change requires a change to the documentation. -- [ ] I have updated the documentation accordingly. -- [ ] I have added tests to cover my changes. -- [ ] All new and existing tests passed. - - - + + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 71a863fa51..cd44bfc1d7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,7 @@ updates: directory: / schedule: interval: monthly + versioning-strategy: increase-if-necessary open-pull-requests-limit: 10 target-branch: dependency-updates labels: @@ -12,11 +13,12 @@ updates: - type:maintenance ignore: - dependency-name: django-mptt # pinned, 0.15 requires Python >= 3.9 + - dependency-name: django + update-types: [ "version-update:semver-major"] # ignore major for django groups: # create a single pull request containing all updates for the optional dependencies optional: patterns: - - coveralls - django-allauth - django-auth-ldap - gunicorn @@ -33,7 +35,6 @@ updates: directory: / schedule: interval: monthly - open-pull-requests-limit: 10 target-branch: dependency-updates labels: - dependencies @@ -58,6 +59,8 @@ updates: - dependency-name: react-bootstrap # rdmo still uses bootstrap 3, which is not supported by react-bootstrap > 0.33.1 - dependency-name: "*" update-types: ["version-update:semver-patch"] # ignore patch versions for all JavaScript dependencies + - dependency-name: react-redux + update-types: ["version-update:semver-major"] # ignore major for react-redux groups: react: patterns: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbccf3b10b..58c4865cf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,18 @@ on: - '.github/workflows/**' - 'rdmo/**' - 'testing/**' + - 'webpack/**' + - .eslintrc.js + - .nvmrc - .pre-commit-config.yaml - conftest.py - package.json - pyproject.toml + types: + - opened + - synchronize + - reopened + - ready_for_review # this is needed to trigger checks, when an auto-generated "draft" PR is set for "ready for review". # Ref: https://docs.github.com/en/actions/using-jobs/using-concurrency concurrency: @@ -23,169 +31,189 @@ concurrency: env: PYTHONDONTWRITEBYTECODE: 1 FORCE_COLOR: 1 # colored output by pytest etc. - CLICOLOR_FORCE: 1 # colored output by ruff jobs: lint: - name: Lint - runs-on: ubuntu-22.04 + # Ref: https://github.com/rdmorganiser/.github/blob/main/.github/workflows/_lint.yml + uses: rdmorganiser/.github/.github/workflows/_lint.yml@main + + build-wheel: + name: Build python wheel + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + # update the version + - name: Get short commit SHA + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + SHA="${{ github.event.pull_request.head.sha }}" + else + SHA="${{ github.sha }}" + fi + echo "SHA=$(git rev-parse --short $SHA)" >> $GITHUB_ENV + - name: Get current version (MAJOR.MINOR.PATCH) + id: current-version + run: echo "current_version=$(grep -Po '(?<=__version__ = ")[\d\w.]+(?=")' rdmo/__init__.py)" >> $GITHUB_OUTPUT + - name: Generate new version (current version + SHA) + id: new-version + run: echo "new_version=${{ steps.current-version.outputs.current_version }}+$SHA" >> $GITHUB_OUTPUT + - name: Update version in rdmo/__init__.py + run: | + sed -i "s/__version__ = .*/__version__ = \"${{ steps.new-version.outputs.new_version }}\"/" rdmo/__init__.py + # build the webpack bundle + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + - run: npm ci && npm run build:prod + # build the wheel + - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: '3.12' cache: pip - - run: python -m pip install --upgrade pip - - run: python -m pip install --editable .[dev] - - name: Set up pre-commit cache - uses: actions/cache@v3 + - run: | + python -m pip install --upgrade pip build[uv] twine + python -m pip --version + - name: Build the wheel + run: python -m build --installer=uv + - name: Check the metadata of wheel and sdist + run: python -m twine check --strict dist/* + - name: Install package from built wheel + run: python -m pip install --no-compile dist/rdmo*.whl # do not create __pycache__/*.pyc files + - name: Write info to step summary + run: | + { + echo -e "# ✓ Wheel successfully built (v${{ steps.new-version.outputs.new_version }})\n\n" + echo '
Information about installed wheel' + echo -e "\n\`\`\`console" + echo "$ python -m pip show --files --verbose rdmo" + python -m pip show --files --verbose rdmo + echo -e "\`\`\`\n
" + } >> $GITHUB_STEP_SUMMARY + - name: Upload wheel as artifact + uses: actions/upload-artifact@v4 with: - path: ~/.cache/pre-commit - key: lint-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Run linters via pre-commit (ruff, eslint) - run: pre-commit run --all-files --color=always + name: wheel + path: dist/rdmo*.whl + if-no-files-found: error + retention-days: 30 test: - runs-on: ubuntu-22.04 + name: "Test (Python: ${{ matrix.python-version }}, DB: ${{ matrix.db-backend }})" + needs: build-wheel + runs-on: ubuntu-24.04 strategy: matrix: python-version: ['3.8', '3.12'] db-backend: [mysql, postgres] - name: "Test (Python: ${{ matrix.python-version }}, DB: ${{ matrix.db-backend }})" - needs: lint steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: wheel + path: dist - name: Install Dependencies run: | - sudo apt update - sudo apt install --yes pandoc texlive-xetex librsvg2-bin - python -m pip install --upgrade pip + sudo apt-get update && sudo apt-get install --yes pandoc texlive-xetex librsvg2-bin pandoc --version - - name: Install rdmo[mysql] and start mysql + python -m pip install --upgrade pip + python -m pip --version + - name: Install rdmo[mysql] from wheel and start mysql run: | - python -m pip install --editable .[ci,mysql] + python -m pip install "$(ls dist/*.whl)[ci,mysql]" sudo systemctl start mysql.service if: matrix.db-backend == 'mysql' - - name: Install rdmo[postgres] and start postgresql + - name: Install rdmo[postgres] from wheel and start postgresql run: | - python -m pip install --editable .[ci,postgres] + python -m pip install "$(ls dist/*.whl)[ci,postgres]" sudo systemctl start postgresql.service pg_isready sudo -u postgres psql --command="CREATE USER postgres_user PASSWORD 'postgres_password' CREATEDB" if: matrix.db-backend == 'postgres' - name: Prepare Env run: | - cp -r testing/media testing/media_root - mkdir testing/log + cp -r testing/media testing/media_root && mkdir testing/log + - name: Run package status tests first + run: | + pytest rdmo/core/tests/test_package_status.py --nomigrations --verbose + if: matrix.python-version == '3.12' && matrix.db-backend == 'postgres' - name: Run Tests run: | pytest -p randomly -p no:cacheprovider --cov --reuse-db --numprocesses=auto --dist=loadscope - coveralls --service=github env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_DB_BACKEND: ${{ matrix.db-backend }} - COVERALLS_FLAG_NAME: '${{ matrix.db-backend }}: ${{ matrix.python-version }}' - COVERALLS_PARALLEL: true - # end-to-end tests - - uses: actions/setup-node@v4 + - name: Upload coverage data to coveralls.io + uses: coverallsapp/github-action@643bc377ffa44ace6394b2b5d0d3950076de9f63 # v2.3.0 with: - node-version: 18 - cache: npm - if: matrix.python-version == '3.12' && matrix.db-backend == 'postgres' + flag-name: '${{ matrix.db-backend }}: ${{ matrix.python-version }}' + parallel: true + + test-e2e: + name: "End-to-end Test (Python: ${{ matrix.python-version }}, DB: ${{ matrix.db-backend }})" + needs: build-wheel + runs-on: ubuntu-24.04 + strategy: + matrix: + python-version: ['3.12'] + db-backend: [postgres] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: wheel + path: dist + - name: Install Dependencies + run: | + sudo apt-get update && sudo apt install --yes pandoc texlive-xetex librsvg2-bin + python -m pip install --upgrade pip + - name: Install rdmo[postgres] from wheel and start postgresql + run: | + python -m pip install "$(ls dist/*.whl)[ci,postgres]" + sudo systemctl start postgresql.service + pg_isready + sudo -u postgres psql --command="CREATE USER postgres_user PASSWORD 'postgres_password' CREATEDB" + - name: Prepare Env + run: | + cp -r testing/media testing/media_root && mkdir testing/log - name: Install e2e tests dependencies run: | - npm install - npm run build:prod - playwright install chromium - if: matrix.python-version == '3.12' && matrix.db-backend == 'postgres' + playwright install --with-deps chromium - run: mkdir screenshots + - name: Collect static files into static root (only required if rdmo is installed from wheel) + run: python testing/manage.py collectstatic --noinput - name: Run end-to-end tests run: pytest -p randomly -p no:cacheprovider --reuse-db --numprocesses=auto --dist=loadscope -m e2e --nomigrations - if: matrix.python-version == '3.12' && matrix.db-backend == 'postgres' env: DJANGO_DEBUG: True GITHUB_DB_BACKEND: ${{ matrix.db-backend }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: screenshots path: screenshots/*.png - if: matrix.python-version == '3.12' && matrix.db-backend == 'postgres' coveralls: name: Indicate completion to coveralls needs: test - runs-on: ubuntu-latest - container: python:3-slim + if: ${{ always() }} + runs-on: ubuntu-24.04 steps: - name: Run Coveralls finish - run: | - python -m pip install coveralls - coveralls --service=github --finish - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - build-wheel: - name: Build python wheel - needs: test - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: Get short commit SHA - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - SHA="${{ github.event.pull_request.head.sha }}" - else - SHA="${{ github.sha }}" - fi - echo "SHA=$(git rev-parse --short $SHA)" >> $GITHUB_ENV - - name: Get current version (MAJOR.MINOR.PATCH) - id: current-version - run: echo "current_version=$(grep -Po '(?<=__version__ = ")[\d\w.]+(?=")' rdmo/__init__.py)" >> $GITHUB_OUTPUT - - name: Generate new version (current version + SHA) - id: new-version - run: echo "new_version=${{ steps.current-version.outputs.current_version }}+$SHA" >> $GITHUB_OUTPUT - - name: Update version in rdmo/__init__.py - run: | - sed -i "s/__version__ = .*/__version__ = \"${{ steps.new-version.outputs.new_version }}\"/" rdmo/__init__.py - - uses: actions/setup-node@v4 - with: - node-version: 18 - cache: npm - - run: npm install - - run: npm run build:prod - - uses: actions/setup-python@v4 - with: - python-version: '3.12' - cache: pip - - run: | - python -m pip install --upgrade pip - python -m pip install .[dev] - - name: Build the wheel - run: python -m build --wheel - - name: Check metadata - run: python -m twine check --strict dist/* - - name: Install package from built wheel - run: python -m pip install --force-reinstall dist/rdmo*.whl - - name: Write info to step summary - run: | - echo -e "# ✓ Wheel successfully built (v${{ steps.new-version.outputs.new_version }})\n\n" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`console" >> $GITHUB_STEP_SUMMARY - echo "$ python -m pip show rdmo" >> $GITHUB_STEP_SUMMARY - python -m pip show rdmo >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - - name: Upload wheel as artifact - uses: actions/upload-artifact@v3 + uses: coverallsapp/github-action@643bc377ffa44ace6394b2b5d0d3950076de9f63 # v2.3.0 with: - name: wheel - path: dist/rdmo*.whl - if-no-files-found: error - retention-days: 30 + parallel-finished: true dev-setup: # Ref: structlog (MIT licensed) @@ -196,7 +224,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip @@ -205,19 +233,26 @@ jobs: dependencies: name: Test installation of all dependencies - runs-on: ubuntu-22.04 + needs: build-wheel + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: wheel + path: dist - name: Install os requirements for python-ldap - run: | - sudo apt update - sudo apt install --yes libldap2-dev libsasl2-dev + run: sudo apt-get update && sudo apt-get install --yes libldap2-dev libsasl2-dev - run: python -m pip install --upgrade pip - - run: python -m pip install .[allauth,shibboleth,ci,dev,gunicorn,ldap,mysql,postgres,pytest] + - name: Install rdmo wheel with all optional dependency groups + run: python -m pip install --no-compile "$(ls dist/*.whl)[allauth,ci,dev,gunicorn,ldap,mysql,postgres,pytest]" + - name: Verify installed packages have compatible dependencies + run: python -m pip check - uses: actions/setup-node@v4 with: node-version: 18 @@ -253,12 +288,13 @@ jobs: if: always() needs: - lint + - build-wheel - test - coveralls - - build-wheel + - test-e2e - dev-setup - dependencies - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: re-actors/alls-green@release/v1 with: diff --git a/.github/workflows/pre-commit-autoupdate.yml b/.github/workflows/pre-commit-autoupdate.yml index a3b72443f9..3001f77681 100644 --- a/.github/workflows/pre-commit-autoupdate.yml +++ b/.github/workflows/pre-commit-autoupdate.yml @@ -21,10 +21,10 @@ jobs: permissions: contents: write # for peter-evans/create-pull-request to create branch pull-requests: write # for peter-evans/create-pull-request to create a PR - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.12" cache: pip @@ -37,10 +37,11 @@ jobs: sed -i -e 's/\]/>/g' updates.log echo -e "## Proposed changes\n\nBumps the pre-commit config with the following updates:\n" > pr-body.md cat updates.log >> pr-body.md - echo -e "\n---\nThis PR is auto-generated once a month." >> pr-body.md + echo -e "\nThis PR is auto-generated once a month.\n\n---" >> pr-body.md + echo -e "\n> [!NOTE]\n> Mark this PR as "ready for review" to trigger additional checks." >> pr-body.md # Ref: https://github.com/peter-evans/create-pull-request - name: Create pull request - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: branch: pre-commit-autoupdate base: dependency-updates @@ -53,8 +54,9 @@ jobs: labels: | dependencies pre-commit - type: maintenance + type:maintenance delete-branch: true + draft: true - name: Write to job summary run: | cat updates.log >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index f5b363cdbf..2af3bf59da 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,8 @@ dist rdmo/management/static +rdmo/projects/static/projects/js/projects.js +rdmo/projects/static/projects/fonts +rdmo/projects/static/projects/css/projects.css + screenshots diff --git a/.nvmrc b/.nvmrc index d4df1049f2..99c98cdd6a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.1.0 +18.20.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de5c983b13..965612070d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: hooks: - id: check-hooks-apply - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-ast - id: check-json @@ -22,16 +22,30 @@ repos: exclude: \.dot$ - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.6 + rev: v0.5.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.54.0 + rev: v8.56.0 hooks: - id: eslint args: [--fix, --color] additional_dependencies: - - eslint@8.54.0 - - eslint-plugin-react@7.33.2 - - react@18.2.0 + - eslint@8.56.0 + - eslint-plugin-react@7.34.0 + - react@18.3.1 + - repo: https://github.com/crate-ci/typos + rev: v1.23.6 + hooks: + - id: typos + exclude: | + (?x)^( + rdmo/locale/.*| + rdmo/.*_de.html$| + rdmo/.*_es.html$| + rdmo/.*_fr.html$| + rdmo/.*_it.html$| + testing/.*.json| + testing/.*.xml + )$ diff --git a/AUTHORS b/AUTHORS index 58d40be67f..c7fdfe433b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,6 +6,7 @@ The core developers are: Olaf Michaelis David Wallace Max Schröder + Claudia Malzer We appreciate contributions from: @@ -40,6 +41,7 @@ The development of RDMO was supported by the following institutions: Istituto Nazionale di Ricerca Metrologica (https://www.inrim.eu) Universität Rostock (https://www.uni-rostock.de) Freie Universität Berlin (https://www.fu-berlin.de) + Julius Kühn-Institut – Bundesforschungsinstitut für Kulturpflanzen (JKI) (https://www.julius-kuehn.de/) Funding for RDMO was received from: diff --git a/CHANGELOG.md b/CHANGELOG.md index da9802b461..d6b276e9eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## [RDMO 2.2.0](https://github.com/rdmorganiser/rdmo/compare/2.1.3...2.2.0) (Jul 25, 2024) + +* Add new projects overview (#865, #355) + * Projects are now displayed in an interactive table which can be filtered and ordered + * The projects API is now paginated (new setting PROJECT_TABLE_PAGE_SIZE) +* Add new import interface to management (#469, #468, #465, ) + * Show detailed information what is new and what changed + * Show a summary of warnings and errors at the top of the page +* Add validation depending on the `value_type` configured for the question + * Validation needs to be enabled using `PROJECT_VALUES_VALIDATION = True` + * Configuration can be adjusted using settings for each value_type +* Enable markdown rendering for titles and texts of elements +* All parent attributes are now added to the full XML export +* Use only available catalogs for project import by users (#455) +* Add workaround for conflict validation for checkboxes (#903) +* Add `merge_attributes` management script to move related items from one attribute to another (#990) +* Add `join_values_inline` tag for views (#964) +* Add `user` and `site` to optionset provider plugins (#430) +* Add short title field for sections and pages for the navigation (#346, #363) +* Add section progress to the navigation +* Add button to add/remove the current site to an element in the management interface for multi site instances (#825) +* Fix a bug with conditions with non-consecutive set_index (when datasets are created and deleted) +* Fix a bug with the progress bar when a section has no pages +* Fix progress action if progress did not change +* Fix bugs with element copy in management (#995, #980) +* Fix typos and missing translations on buttons in management interface (#1020, #944) +* Fix link target for links in management interface (#1007) +* Fix textarea resizing (#1021) +* Fix export links in management (#915) +* Fix typos (#1001) +* Use ACCOUNT_FORMS instead of ACCOUNT_SIGNUP_FORM_CLASS in settings +* Remove local hosts from ALLOWED_HOSTS settings +* Improve admin interface (#942, #918) +* Update default home page + ## [RDMO 2.1.3](https://github.com/rdmorganiser/rdmo/compare/2.1.2...2.1.3) (Feb 13, 2024) * Fix the migration of options with additional_input (#912) diff --git a/CITATION.cff b/CITATION.cff index a468ea083f..7d7e7d77fa 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -21,6 +21,9 @@ authors: given-names: Heinz-Alexander orcid: https://orcid.org/0000-0003-4397-027X + - family-names: Malzer + given-names: Claudia + - family-names: Lanza given-names: Giacomo orcid: https://orcid.org/0000-0002-2239-3955 @@ -32,8 +35,8 @@ authors: given-names: Dario orcid: https://orcid.org/0000-0003-4499-8573 - - family-names: Harry - given-names: Enke + - family-names: Enke + given-names: Harry orcid: https://orcid.org/0000-0002-2366-8316 title: Research Data Management Organiser (RDMO) diff --git a/NOTICE b/NOTICE index 6df73d10b6..6de137fc48 100644 --- a/NOTICE +++ b/NOTICE @@ -4,7 +4,7 @@ THIS NOTICE APPLIES TO ALL FILES CONTAINED IN THIS AND UNDERLYING FOLDERS RDMO - Research Data Management Organiser -Copyright (c) 2015-2023 RDMO Arbeitsgemeinschaft +Copyright (c) 2015-2024 RDMO Arbeitsgemeinschaft Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/conftest.py b/conftest.py index 7a2d7ec8d9..5dbd3652e6 100644 --- a/conftest.py +++ b/conftest.py @@ -34,7 +34,7 @@ def fixtures(): @pytest.fixture(scope='session') -def django_db_setup(django_db_setup, django_db_blocker, fixtures): +def django_db_setup(django_db_setup, django_db_blocker, fixtures): # noqa: PT004 - pytest-django requires this name "django_db_setup" """Populate database with test data from fixtures directories.""" with django_db_blocker.unblock(): call_command('loaddata', *fixtures) diff --git a/package-lock.json b/package-lock.json index 510d52f058..dbb5e68b40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,44 +9,48 @@ "license": "Apache-2.0", "dependencies": { "@codemirror/lang-html": "^6.4.2", - "@codemirror/lang-javascript": "^6.1.4", - "@uiw/react-codemirror": "^4.19.9", + "@codemirror/lang-javascript": "^6.2.2", + "@uiw/react-codemirror": "^4.23.0", "bootstrap-sass": "^3.4.1", - "classnames": "^2.3.2", + "classnames": "^2.5.1", + "date-fns": "^3.6.0", "font-awesome": "4.7.0", - "jquery": "^3.6.0", - "js-cookie": "^2.2.1", + "jquery": "^3.7.1", + "js-cookie": "^3.0.5", "lodash": "^4.17.21", "popper.js": "^1.16.1", "prop-types": "^15.7.2", - "react": "^18.2.0", + "react": "^18.3.1", "react-bootstrap": "0.33.1", + "react-datepicker": "7.3.0", + "react-diff-viewer-continued": "^3.4.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", - "react-dom": "^18.2.0", - "react-redux": "^7.2.4", - "react-select": "^5.7.0", + "react-dom": "^18.3.1", + "react-dropzone": "^14.2.3", + "react-redux": "^8.1.3", + "react-select": "^5.8.0", "redux": "^4.1.1", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0" }, "devDependencies": { - "@babel/cli": "^7.23.4", - "@babel/core": "^7.23.3", - "@babel/preset-env": "^7.23.3", - "@babel/preset-react": "^7.23.3", + "@babel/cli": "^7.24.1", + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.25.3", + "@babel/preset-react": "^7.24.1", "babel-loader": "^9.1.3", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^5.2.0", - "eslint": "^8.54.0", - "eslint-plugin-react": "^7.33.2", + "copy-webpack-plugin": "^12.0.2", + "css-loader": "^7.1.1", + "eslint": "~8.56.0", + "eslint-plugin-react": "^7.35.0", "file-loader": "^6.2.0", - "mini-css-extract-plugin": "^1.4.0", - "sass": "^1.55.0", - "sass-loader": "^11.0.1", - "webpack": "^5.89.0", + "mini-css-extract-plugin": "^2.9.0", + "sass": "^1.77.1", + "sass-loader": "^16.0.0", + "webpack": "^5.93.0", "webpack-cli": "^5.1.4", - "webpack-merge": "5.10.0" + "webpack-merge": "6.0.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -62,6 +66,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "devOptional": true, "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -71,12 +76,12 @@ } }, "node_modules/@babel/cli": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.4.tgz", - "integrity": "sha512-j3luA9xGKCXVyCa5R7lJvOMM+Kc2JEnAEIgz2ggtjQ/j5YUVgfsg/WsG95bbsgq7YLHuiCOzMnoSasuY16qiCw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.24.1.tgz", + "integrity": "sha512-HbmrtxyFUr34LwAlV9jS+sSIjUp4FpdtIMGwgufY3AsxrIfsh/HxlMTywsONAZsU0RMYbZtbZFpUCrSGs7o0EA==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.25", "commander": "^4.0.1", "convert-source-map": "^2.0.0", "fs-readdir-recursive": "^1.1.0", @@ -106,40 +111,42 @@ "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", - "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "devOptional": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "devOptional": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -157,16 +164,17 @@ "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "devOptional": true }, "node_modules/@babel/generator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", - "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dependencies": { - "@babel/types": "^7.23.4", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -174,50 +182,52 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "devOptional": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -229,6 +239,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "devOptional": true, "dependencies": { "yallist": "^3.0.2" } @@ -236,22 +247,21 @@ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", + "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/traverse": "^7.25.0", "semver": "^6.3.1" }, "engines": { @@ -262,12 +272,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", + "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-annotate-as-pure": "^7.24.7", "regexpu-core": "^5.3.1", "semver": "^6.3.1" }, @@ -279,9 +289,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -294,70 +304,41 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", "dev": true, "dependencies": { - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "devOptional": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" @@ -367,34 +348,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", + "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-wrap-function": "^7.25.0", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -404,14 +386,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -421,107 +403,104 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "devOptional": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "devOptional": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", + "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", "dev": true, "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz", - "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "devOptional": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.4", - "@babel/types": "^7.23.4" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", - "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -529,13 +508,44 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", + "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", + "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", + "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -545,14 +555,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -562,13 +572,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", - "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", + "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -653,12 +663,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -668,12 +678,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -710,6 +720,7 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -839,12 +850,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -854,15 +865,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz", - "integrity": "sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", + "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-remap-async-to-generator": "^7.25.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -872,14 +883,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -889,12 +900,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -904,12 +915,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", - "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", + "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -919,13 +930,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -935,13 +946,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", - "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -952,19 +963,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz", - "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", + "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/traverse": "^7.25.0", "globals": "^11.1.0" }, "engines": { @@ -975,13 +983,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -991,12 +999,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1006,13 +1014,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1022,12 +1030,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1036,13 +1044,29 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", + "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", - "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1053,13 +1077,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1069,12 +1093,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", - "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -1085,12 +1109,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", - "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1100,14 +1125,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", + "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.1" }, "engines": { "node": ">=6.9.0" @@ -1117,12 +1142,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", - "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -1133,12 +1158,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", + "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1148,12 +1173,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", - "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -1164,12 +1189,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1179,13 +1204,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1195,14 +1220,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-simple-access": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1212,15 +1237,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", - "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", + "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", "dev": true, "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-transforms": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -1230,13 +1255,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1246,13 +1271,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1262,12 +1287,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1277,12 +1302,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", - "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -1293,12 +1318,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", - "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -1309,16 +1334,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" + "@babel/plugin-transform-parameters": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1328,13 +1352,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1344,12 +1368,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", - "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -1360,13 +1384,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", - "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -1377,12 +1401,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1392,13 +1416,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1408,14 +1432,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", - "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", "dev": true, "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -1426,12 +1450,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1441,12 +1465,12 @@ } }, "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", - "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", + "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1490,13 +1514,13 @@ } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", - "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", + "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1506,12 +1530,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "regenerator-transform": "^0.15.2" }, "engines": { @@ -1522,12 +1546,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1537,12 +1561,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1552,13 +1576,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1568,12 +1592,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1583,12 +1607,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1598,12 +1622,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1613,12 +1637,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1628,13 +1652,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1644,13 +1668,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1660,13 +1684,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -1676,26 +1700,28 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.3.tgz", - "integrity": "sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", + "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -1707,59 +1733,60 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.3", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.3", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.3", - "@babel/plugin-transform-classes": "^7.23.3", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.3", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.3", - "@babel/plugin-transform-for-of": "^7.23.3", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.3", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.3", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.3", - "@babel/plugin-transform-numeric-separator": "^7.23.3", - "@babel/plugin-transform-object-rest-spread": "^7.23.3", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.3", - "@babel/plugin-transform-optional-chaining": "^7.23.3", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.3", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.0", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.25.0", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -1784,17 +1811,17 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", - "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", + "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-react-display-name": "^7.23.3", - "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-transform-react-display-name": "^7.24.1", + "@babel/plugin-transform-react-jsx": "^7.23.4", "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.23.3" + "@babel/plugin-transform-react-pure-annotations": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -1833,32 +1860,29 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", - "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", - "dependencies": { - "@babel/code-frame": "^7.23.4", - "@babel/generator": "^7.23.4", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.4", - "@babel/types": "^7.23.4", - "debug": "^4.1.0", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1866,12 +1890,12 @@ } }, "node_modules/@babel/types": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz", - "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1934,15 +1958,15 @@ } }, "node_modules/@codemirror/lang-javascript": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.4.tgz", - "integrity": "sha512-OxLf7OfOZBTMRMi6BO/F72MNGmgOd9B0vetOLvHsDACFXayBzW8fm8aWnDM0yuy68wTK03MBf4HbjSBNRG5q7A==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", + "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } @@ -1981,9 +2005,9 @@ } }, "node_modules/@codemirror/state": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", - "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" }, "node_modules/@codemirror/theme-one-dark": { "version": "6.1.1", @@ -1997,12 +2021,12 @@ } }, "node_modules/@codemirror/view": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.1.tgz", - "integrity": "sha512-bzfSjJn9dAADVpabLKWKNmMG4ibyTV2e3eOGowjElNPTdTkSbi6ixPYHm2u0ADcETfKsi2/R84Rkmi91dH9yEg==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.1.tgz", + "integrity": "sha512-sBfP4rniPBRQzNakwuQEqjEuiJDWJyF2kqLLqij4WXRoVwPPJfjx966Eq3F7+OPQxDtMt/Q9MWLoZLWjeveBlg==", "dependencies": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.0.0", + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, @@ -2016,25 +2040,21 @@ } }, "node_modules/@emotion/babel-plugin": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", - "integrity": "sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "dependencies": { "@babel/helper-module-imports": "^7.16.7", - "@babel/plugin-syntax-jsx": "^7.17.12", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.1.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "stylis": "4.2.0" } }, "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { @@ -2057,26 +2077,38 @@ } }, "node_modules/@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "dependencies": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/css": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz", + "integrity": "sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==", + "dependencies": { + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1" } }, "node_modules/@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "node_modules/@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { "version": "11.10.5", @@ -2106,26 +2138,26 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "dependencies": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.0", @@ -2136,14 +2168,14 @@ } }, "node_modules/@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "node_modules/@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", @@ -2170,9 +2202,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -2193,9 +2225,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -2208,9 +2240,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2229,6 +2261,37 @@ "@floating-ui/core": "^1.2.1" } }, + "node_modules/@floating-ui/react": { + "version": "0.26.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.19.tgz", + "integrity": "sha512-Jk6zITdjjIvjO/VdQFvpRaD3qPwOHH6AoDHxjhpy+oK4KFgaSP871HYWUAPdnLmx1gQ+w/pB312co3tVml+BXA==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.1", + "@floating-ui/utils": "^0.2.4", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", + "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -2266,6 +2329,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "devOptional": true, "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -2283,9 +2347,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.0.tgz", - "integrity": "sha512-SfJxIxNVYLTsKwzB3MoOQ1yxf4w/E6MdkvTgrgAt1bfxjSrLUoHMKrDOykwN14q65waezZIdqDneUIPh4/sKxg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -2301,14 +2365,14 @@ } }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -2320,9 +2384,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2434,8 +2498,20 @@ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" }, - "node_modules/@types/eslint": { - "version": "8.4.2", + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/eslint": { + "version": "8.4.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.2.tgz", "integrity": "sha512-Z1nseZON+GEnFjJc04sv4NSALGjhFwy6K0HXt7qsn5ArfAKtb63dXNJHf+1YW6IpOIYRBGUbu3GwJdj8DGnCjA==", "dev": true, @@ -2501,17 +2577,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-redux": { - "version": "7.1.24", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", - "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", @@ -2525,10 +2590,15 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.19.9", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.19.9.tgz", - "integrity": "sha512-O4yAgVpD3Pon4t4+lwZ2MTGg2TeU/Jv8YzKS9ap4fP/WMTVrKmpdq+DOafbhZSlhmU0XGfQPPJ4WX6rtZgx3Rw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.0.tgz", + "integrity": "sha512-+k5nkRpUWGaHr1JWT8jcKsVewlXw5qBgSopm9LW8fZ6KnSNZBycz8kHxh0+WSvckmXEESGptkIsb7dlkmJT/hQ==", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -2538,6 +2608,9 @@ "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, "peerDependencies": { "@codemirror/autocomplete": ">=6.0.0", "@codemirror/commands": ">=6.0.0", @@ -2549,17 +2622,20 @@ } }, "node_modules/@uiw/react-codemirror": { - "version": "4.19.9", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.19.9.tgz", - "integrity": "sha512-U+A1fSfELMFFs5a+ZOPwCJKZMYaMy6QHOfNOOV7WhSveM7GYHT970GjTfs2dn1LlvhebwyK9ei0rCXZdsI9n9Q==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.0.tgz", + "integrity": "sha512-MnqTXfgeLA3fsUUQjqjJgemEuNyoGALgsExVm0NQAllAAi1wfj+IoKFeK+h3XXMlTFRCFYOUh4AHDv0YXJLsOg==", "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.19.9", + "@uiw/codemirror-extensions-basic-setup": "4.23.0", "codemirror": "^6.0.0" }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, "peerDependencies": { "@babel/runtime": ">=7.11.0", "@codemirror/state": ">=6.0.0", @@ -2577,9 +2653,9 @@ "dev": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -2599,9 +2675,9 @@ "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { @@ -2622,15 +2698,15 @@ "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { @@ -2658,28 +2734,28 @@ "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -2687,24 +2763,24 @@ } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -2713,12 +2789,12 @@ } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -2790,10 +2866,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -2912,28 +2988,32 @@ "dev": true }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -2943,15 +3023,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" }, "engines": { @@ -2962,30 +3062,34 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -2995,20 +3099,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", - "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -3048,13 +3154,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.6.2", "semver": "^6.3.1" }, "peerDependencies": { @@ -3062,25 +3168,25 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3", - "core-js-compat": "^3.33.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.6.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -3126,21 +3232,22 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "devOptional": true, "funding": [ { "type": "opencollective", @@ -3156,10 +3263,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -3175,14 +3282,19 @@ "dev": true }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3197,9 +3309,10 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001564", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", - "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "devOptional": true, "funding": [ { "type": "opencollective", @@ -3265,9 +3378,9 @@ } }, "node_modules/classnames": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, "node_modules/clone-deep": { "version": "4.0.1", @@ -3283,6 +3396,14 @@ "node": ">=6" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/codemirror": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", @@ -3346,20 +3467,20 @@ } }, "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, "dependencies": { - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^13.1.1", + "globby": "^14.0.0", "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "engines": { - "node": ">= 14.15.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", @@ -3389,12 +3510,12 @@ "hasInstallScript": true }, "node_modules/core-js-compat": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", - "integrity": "sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==", + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz", + "integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==", "dev": true, "dependencies": { - "browserslist": "^4.22.1" + "browserslist": "^4.23.3" }, "funding": { "type": "opencollective", @@ -3436,49 +3557,38 @@ } }, "node_modules/css-loader": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.1.tgz", + "integrity": "sha512-OxIR5P2mjO1PSXk44bWuQ8XtMK4dpEqpIyERCx3ewOo3I8EmbcxMPUc5ScLtQfgXtOojoMv57So4V/C02HQLsw==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.27.0 || ^5.0.0" - } - }, - "node_modules/css-loader/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } } }, "node_modules/css-loader/node_modules/semver": { @@ -3513,6 +3623,66 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3541,17 +3711,20 @@ "dev": true }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -3571,16 +3744,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", "engines": { - "node": ">=8" + "node": ">=0.3.1" } }, "node_modules/dnd-core": { @@ -3614,9 +3783,10 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.592", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.592.tgz", - "integrity": "sha512-D3NOkROIlF+d5ixnz7pAf3Lu/AuWpd6AYgI9O67GQXMXTcCP1gJQRotOq35eQy5Sb4hez33XH1YdTtILA7Udww==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", + "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", + "devOptional": true }, "node_modules/emojis-list": { "version": "3.0.0", @@ -3628,9 +3798,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -3661,50 +3831,57 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -3713,26 +3890,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-iterator-helpers": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", - "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", "dev": true, "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", + "internal-slot": "^1.0.7", "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-module-lexer": { @@ -3741,27 +3942,39 @@ "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { @@ -3782,9 +3995,10 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "devOptional": true, "engines": { "node": ">=6" } @@ -3798,15 +4012,15 @@ } }, "node_modules/eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -3853,33 +4067,35 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "version": "7.35.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", + "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -3904,12 +4120,12 @@ } }, "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -4324,10 +4540,21 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -4577,33 +4804,39 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "devOptional": true, "engines": { "node": ">=6.9.0" } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -4674,28 +4907,29 @@ } }, "node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", "dev": true, "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "node_modules/globby/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, "engines": { "node": ">=12" @@ -4704,6 +4938,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -4728,17 +4974,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -4757,21 +4992,21 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -4793,12 +5028,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4808,10 +5043,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -4922,13 +5156,13 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -4953,14 +5187,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5039,11 +5275,26 @@ } }, "node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, "dependencies": { - "has": "^1.0.3" + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5113,18 +5364,21 @@ } }, "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -5195,21 +5449,27 @@ } }, "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5246,12 +5506,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -5261,10 +5521,13 @@ } }, "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5282,13 +5545,16 @@ } }, "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5367,14 +5633,17 @@ } }, "node_modules/jquery": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", - "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, "node_modules/js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -5425,6 +5694,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "devOptional": true, "bin": { "json5": "lib/cli.js" }, @@ -5459,15 +5729,6 @@ "node": ">=0.10.0" } }, - "node_modules/klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5638,52 +5899,23 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", - "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", "dev": true, "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "webpack-sources": "^1.1.0" + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.4.0 || ^5.0.0" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" + "webpack": "^5.0.0" } }, "node_modules/minimatch": { @@ -5704,10 +5936,16 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -5728,9 +5966,10 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "devOptional": true }, "node_modules/normalize-path": { "version": "3.0.0", @@ -5768,13 +6007,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -5786,28 +6025,29 @@ } }, "node_modules/object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -5816,28 +6056,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -5977,9 +6204,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -6024,10 +6251,19 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "funding": [ { @@ -6037,10 +6273,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -6049,9 +6289,9 @@ } }, "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, "engines": { "node": "^10 || ^12 || >= 14" @@ -6061,9 +6301,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -6078,9 +6318,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -6108,9 +6348,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -6196,9 +6436,9 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -6237,6 +6477,41 @@ "loose-envify": "^1.0.0" } }, + "node_modules/react-datepicker": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.3.0.tgz", + "integrity": "sha512-EqRKLAtLZUTztiq6a+tjSjQX9ES0Xd229JPckAtyZZ4GoY3rtvNWAzkYZnQUf6zTWT50Ki0+t+W9VRQIkSJLfg==", + "dependencies": { + "@floating-ui/react": "^0.26.2", + "clsx": "^2.1.0", + "date-fns": "^3.3.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, + "node_modules/react-diff-viewer-continued": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz", + "integrity": "sha512-kMZmUyb3Pv5L9vUtCfIGYsdOHs8mUojblGy1U1Sm0D7FhAOEsH9QhnngEIRo5hXWIPNGupNRJls1TJ6Eqx84eg==", + "dependencies": { + "@emotion/css": "^11.11.2", + "classnames": "^2.3.2", + "diff": "^5.1.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dnd": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", @@ -6275,15 +6550,31 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" }, "peerDependencies": { - "react": "^18.2.0" + "react": ">= 16.8 || 18.0.0" } }, "node_modules/react-is": { @@ -6296,6 +6587,19 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-onclickoutside": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz", + "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, "node_modules/react-overlays": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.9.3.tgz", @@ -6341,38 +6645,52 @@ } }, "node_modules/react-redux": { - "version": "7.2.8", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz", - "integrity": "sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" }, "peerDependencies": { - "react": "^16.8.3 || ^17 || ^18" + "@types/react": "^16.8 || ^17.0 || ^18.0", + "@types/react-dom": "^16.8 || ^17.0 || ^18.0", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0", + "react-native": ">=0.59", + "redux": "^4 || ^5.0.0-beta.0" }, "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, "react-dom": { "optional": true }, "react-native": { "optional": true + }, + "redux": { + "optional": true } } }, "node_modules/react-redux/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/react-select": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.0.tgz", - "integrity": "sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", @@ -6477,15 +6795,16 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", - "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", "which-builtin-type": "^1.1.3" }, @@ -6529,14 +6848,15 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -6678,13 +6998,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -6701,23 +7021,26 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/sass": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", - "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "version": "1.77.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz", + "integrity": "sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -6728,33 +7051,33 @@ "sass": "sass.js" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/sass-loader": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-11.1.1.tgz", - "integrity": "sha512-fOCp/zLmj1V1WHDZbUbPgrZhA7HKXHEqkslzB+05U5K9SbSbcmH91C7QLW31AsXikxUMaxXRhhcqWZAxUMLDyA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", + "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", "dev": true, "dependencies": { - "klona": "^2.0.4", "neo-async": "^2.6.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "fibers": ">= 3.1.0", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0", + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", "sass": "^1.3.0", + "sass-embedded": "*", "webpack": "^5.0.0" }, "peerDependenciesMeta": { - "fibers": { + "@rspack/core": { "optional": true }, "node-sass": { @@ -6762,13 +7085,19 @@ }, "sass": { "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true } } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } @@ -6830,43 +7159,47 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6906,14 +7239,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6928,12 +7265,6 @@ "node": ">=6" } }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6963,33 +7294,51 @@ } }, "node_modules/string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4" + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -6999,28 +7348,31 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7051,14 +7403,14 @@ } }, "node_modules/style-mod": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", - "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" }, "node_modules/stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/supports-color": { "version": "5.5.0", @@ -7082,6 +7434,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -7092,9 +7449,9 @@ } }, "node_modules/terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -7110,16 +7467,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -7193,6 +7550,11 @@ "node": ">=8.0" } }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7218,29 +7580,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -7250,16 +7613,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -7269,14 +7633,20 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7351,10 +7721,23 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "devOptional": true, "funding": [ { "type": "opencollective", @@ -7370,8 +7753,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -7402,6 +7785,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7422,9 +7813,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -7435,34 +7826,34 @@ } }, "node_modules/webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -7535,7 +7926,7 @@ "node": ">=14" } }, - "node_modules/webpack-merge": { + "node_modules/webpack-cli/node_modules/webpack-merge": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", @@ -7549,6 +7940,20 @@ "node": ">=10.0.0" } }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", @@ -7608,13 +8013,13 @@ } }, "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", "dev": true, "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.0.5", "is-finalizationregistry": "^1.0.2", @@ -7623,8 +8028,8 @@ "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -7634,31 +8039,34 @@ } }, "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7668,9 +8076,9 @@ } }, "node_modules/wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, "node_modules/wrappy": { @@ -7717,18 +8125,19 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "devOptional": true, "requires": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "@babel/cli": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.4.tgz", - "integrity": "sha512-j3luA9xGKCXVyCa5R7lJvOMM+Kc2JEnAEIgz2ggtjQ/j5YUVgfsg/WsG95bbsgq7YLHuiCOzMnoSasuY16qiCw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.24.1.tgz", + "integrity": "sha512-HbmrtxyFUr34LwAlV9jS+sSIjUp4FpdtIMGwgufY3AsxrIfsh/HxlMTywsONAZsU0RMYbZtbZFpUCrSGs7o0EA==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.25", "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", "chokidar": "^3.4.0", "commander": "^4.0.1", @@ -7748,34 +8157,36 @@ } }, "@babel/code-frame": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.4.tgz", - "integrity": "sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "requires": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.3.tgz", - "integrity": "sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==" + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "devOptional": true }, "@babel/core": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.3.tgz", - "integrity": "sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "devOptional": true, "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.2", - "@babel/parser": "^7.23.3", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.3", - "@babel/types": "^7.23.3", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -7786,59 +8197,62 @@ "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "devOptional": true } } }, "@babel/generator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.4.tgz", - "integrity": "sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "requires": { - "@babel/types": "^7.23.4", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "requires": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" } } } }, "@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", - "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", "dev": true, "requires": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", - "requires": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "devOptional": true, + "requires": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -7847,6 +8261,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "devOptional": true, "requires": { "yallist": "^3.0.2" } @@ -7854,42 +8269,41 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "devOptional": true } } }, "@babel/helper-create-class-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", - "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", + "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/traverse": "^7.25.0", "semver": "^6.3.1" } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", - "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz", + "integrity": "sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-annotate-as-pure": "^7.24.7", "regexpu-core": "^5.3.1", "semver": "^6.3.1" } }, "@babel/helper-define-polyfill-provider": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", - "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", "dev": true, "requires": { "@babel/helper-compilation-targets": "^7.22.6", @@ -7899,197 +8313,197 @@ "resolve": "^1.14.2" } }, - "@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" - }, - "@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "requires": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "requires": { - "@babel/types": "^7.22.5" - } - }, "@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", "dev": true, "requires": { - "@babel/types": "^7.23.0" + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" } }, "@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "requires": { - "@babel/types": "^7.22.15" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "devOptional": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" } }, "@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dev": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" } }, "@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==" + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true }, "@babel/helper-remap-async-to-generator": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", - "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz", + "integrity": "sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-wrap-function": "^7.22.20" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-wrap-function": "^7.25.0", + "@babel/traverse": "^7.25.0" } }, "@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" } }, "@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "devOptional": true, "requires": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dev": true, "requires": { - "@babel/types": "^7.22.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "requires": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, "@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==" + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==" }, "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==" }, "@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==" + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "devOptional": true }, "@babel/helper-wrap-function": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", - "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz", + "integrity": "sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ==", "dev": true, "requires": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.15", - "@babel/types": "^7.22.19" + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.0", + "@babel/types": "^7.25.0" } }, "@babel/helpers": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.4.tgz", - "integrity": "sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "devOptional": true, "requires": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.4", - "@babel/types": "^7.23.4" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" } }, "@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "requires": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" } }, "@babel/parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.4.tgz", - "integrity": "sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==" + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "requires": { + "@babel/types": "^7.25.2" + } + }, + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz", + "integrity": "sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.3" + } + }, + "@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz", + "integrity": "sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.8" + } }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz", + "integrity": "sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" } }, "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", - "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz", + "integrity": "sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.0" } }, "@babel/plugin-proposal-private-property-in-object": { @@ -8145,21 +8559,21 @@ } }, "@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-syntax-import-meta": { @@ -8184,6 +8598,7 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.22.5" } @@ -8271,392 +8686,399 @@ } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-async-generator-functions": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz", - "integrity": "sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz", + "integrity": "sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q==", "dev": true, "requires": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-remap-async-to-generator": "^7.25.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/traverse": "^7.25.0" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", - "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz", + "integrity": "sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-class-static-block": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", - "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-class-static-block": "^7.14.5" } }, "@babel/plugin-transform-classes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.3.tgz", - "integrity": "sha512-FGEQmugvAEu2QtgtU0uTASXevfLMFfBeVCIIdcQhn/uBQsMTjBajdnAtanQlOcuihWh10PZ7+HWvc7NtBwP74w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", + "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/traverse": "^7.25.0", "globals": "^11.1.0" } }, "@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" } }, "@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz", + "integrity": "sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-transform-dynamic-import": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", - "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", "dev": true, "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-export-namespace-from": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", - "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" } }, "@babel/plugin-transform-for-of": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", - "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" } }, "@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "version": "7.25.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz", + "integrity": "sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/traverse": "^7.25.1" } }, "@babel/plugin-transform-json-strings": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", - "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-json-strings": "^7.8.3" } }, "@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz", + "integrity": "sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", - "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-simple-access": "^7.24.7" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", - "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", + "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-transforms": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.0" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", - "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" } }, "@babel/plugin-transform-numeric-separator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", - "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", "dev": true, "requires": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" + "@babel/plugin-transform-parameters": "^7.24.7" } }, "@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" } }, "@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", - "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" } }, "@babel/plugin-transform-optional-chaining": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", - "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" } }, "@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-private-property-in-object": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", - "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" } }, "@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-react-display-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz", - "integrity": "sha512-GnvhtVfA2OAtzdX58FJxU19rhoGeQzyVndw3GgtdECQvQFXPEZIOVULHVZGAYmOgmqjXpVpfocAbSjh99V/Fqw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", + "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" } }, "@babel/plugin-transform-react-jsx": { @@ -8682,140 +9104,142 @@ } }, "@babel/plugin-transform-react-pure-annotations": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz", - "integrity": "sha512-qMFdSS+TUhB7Q/3HVPnEdYJDQIk57jkntAwSuz9xfSE4n+3I+vHYCli3HoHawN1Z3RfCz/y1zXA/JXjG6cVImQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", + "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" } }, "@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.7", "regenerator-transform": "^0.15.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.8" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" } }, "@babel/preset-env": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.3.tgz", - "integrity": "sha512-ovzGc2uuyNfNAs/jyjIGxS8arOHS5FENZaNn4rtE7UdKMMkqHCvboHfcuhWLZNX5cB44QfcGNWjaevxMzzMf+Q==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.3.tgz", + "integrity": "sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.3", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.0", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.0", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -8827,59 +9251,60 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.3", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.3", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.3", - "@babel/plugin-transform-classes": "^7.23.3", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.3", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.3", - "@babel/plugin-transform-for-of": "^7.23.3", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.3", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.3", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", - "@babel/plugin-transform-modules-umd": "^7.23.3", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.3", - "@babel/plugin-transform-numeric-separator": "^7.23.3", - "@babel/plugin-transform-object-rest-spread": "^7.23.3", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.3", - "@babel/plugin-transform-optional-chaining": "^7.23.3", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.3", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.0", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.25.0", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.0", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.25.0", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.6", - "babel-plugin-polyfill-corejs3": "^0.8.5", - "babel-plugin-polyfill-regenerator": "^0.5.3", - "core-js-compat": "^3.31.0", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" } }, @@ -8895,17 +9320,17 @@ } }, "@babel/preset-react": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.23.3.tgz", - "integrity": "sha512-tbkHOS9axH6Ysf2OUEqoSZ6T3Fa2SrNH6WTWSPBboxKzdxNc9qOICeLXkNG0ZEwbQ1HY8liwOce4aN/Ceyuq6w==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", + "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-transform-react-display-name": "^7.23.3", - "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-transform-react-display-name": "^7.24.1", + "@babel/plugin-transform-react-jsx": "^7.23.4", "@babel/plugin-transform-react-jsx-development": "^7.22.5", - "@babel/plugin-transform-react-pure-annotations": "^7.23.3" + "@babel/plugin-transform-react-pure-annotations": "^7.24.1" } }, "@babel/regjsgen": { @@ -8932,39 +9357,36 @@ } }, "@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "requires": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" } }, "@babel/traverse": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.4.tgz", - "integrity": "sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==", - "requires": { - "@babel/code-frame": "^7.23.4", - "@babel/generator": "^7.23.4", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.4", - "@babel/types": "^7.23.4", - "debug": "^4.1.0", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.4.tgz", - "integrity": "sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" } }, @@ -9018,15 +9440,15 @@ } }, "@codemirror/lang-javascript": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.4.tgz", - "integrity": "sha512-OxLf7OfOZBTMRMi6BO/F72MNGmgOd9B0vetOLvHsDACFXayBzW8fm8aWnDM0yuy68wTK03MBf4HbjSBNRG5q7A==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", + "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", "requires": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } @@ -9065,9 +9487,9 @@ } }, "@codemirror/state": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", - "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" }, "@codemirror/theme-one-dark": { "version": "6.1.1", @@ -9081,12 +9503,12 @@ } }, "@codemirror/view": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.1.tgz", - "integrity": "sha512-bzfSjJn9dAADVpabLKWKNmMG4ibyTV2e3eOGowjElNPTdTkSbi6ixPYHm2u0ADcETfKsi2/R84Rkmi91dH9yEg==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.1.tgz", + "integrity": "sha512-sBfP4rniPBRQzNakwuQEqjEuiJDWJyF2kqLLqij4WXRoVwPPJfjx966Eq3F7+OPQxDtMt/Q9MWLoZLWjeveBlg==", "requires": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.0.0", + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, @@ -9097,22 +9519,21 @@ "dev": true }, "@emotion/babel-plugin": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", - "integrity": "sha512-xE7/hyLHJac7D2Ve9dKroBBZqBT7WuPQmWcq7HSGb84sUuP4mlOWoB8dvVfD9yk5DHkU1m6RW7xSoDtnQHNQeA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "requires": { "@babel/helper-module-imports": "^7.16.7", - "@babel/plugin-syntax-jsx": "^7.17.12", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.1.3" + "stylis": "4.2.0" }, "dependencies": { "escape-string-regexp": { @@ -9128,26 +9549,38 @@ } }, "@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "requires": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "@emotion/css": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz", + "integrity": "sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==", + "requires": { + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1" } }, "@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "@emotion/react": { "version": "11.10.5", @@ -9165,26 +9598,26 @@ } }, "@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "requires": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.0", @@ -9193,14 +9626,14 @@ "requires": {} }, "@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, "@eslint-community/eslint-utils": { "version": "4.4.0", @@ -9218,9 +9651,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -9235,9 +9668,9 @@ }, "dependencies": { "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -9246,9 +9679,9 @@ } }, "@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true }, "@floating-ui/core": { @@ -9264,6 +9697,29 @@ "@floating-ui/core": "^1.2.1" } }, + "@floating-ui/react": { + "version": "0.26.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.19.tgz", + "integrity": "sha512-Jk6zITdjjIvjO/VdQFvpRaD3qPwOHH6AoDHxjhpy+oK4KFgaSP871HYWUAPdnLmx1gQ+w/pB312co3tVml+BXA==", + "requires": { + "@floating-ui/react-dom": "^2.1.1", + "@floating-ui/utils": "^0.2.4", + "tabbable": "^6.0.0" + } + }, + "@floating-ui/react-dom": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "requires": { + "@floating-ui/dom": "^1.0.0" + } + }, + "@floating-ui/utils": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", + "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==" + }, "@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -9291,6 +9747,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "devOptional": true, "requires": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -9302,9 +9759,9 @@ "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" }, "@jridgewell/set-array": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.0.tgz", - "integrity": "sha512-SfJxIxNVYLTsKwzB3MoOQ1yxf4w/E6MdkvTgrgAt1bfxjSrLUoHMKrDOykwN14q65waezZIdqDneUIPh4/sKxg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" }, "@jridgewell/source-map": { "version": "0.3.5", @@ -9317,14 +9774,14 @@ }, "dependencies": { "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" } } } @@ -9335,9 +9792,9 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -9440,6 +9897,12 @@ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" }, + "@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true + }, "@types/eslint": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.2.tgz", @@ -9507,17 +9970,6 @@ "csstype": "^3.0.2" } }, - "@types/react-redux": { - "version": "7.1.24", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz", - "integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==", - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, "@types/react-transition-group": { "version": "4.4.4", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz", @@ -9531,10 +9983,15 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + }, "@uiw/codemirror-extensions-basic-setup": { - "version": "4.19.9", - "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.19.9.tgz", - "integrity": "sha512-O4yAgVpD3Pon4t4+lwZ2MTGg2TeU/Jv8YzKS9ap4fP/WMTVrKmpdq+DOafbhZSlhmU0XGfQPPJ4WX6rtZgx3Rw==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.0.tgz", + "integrity": "sha512-+k5nkRpUWGaHr1JWT8jcKsVewlXw5qBgSopm9LW8fZ6KnSNZBycz8kHxh0+WSvckmXEESGptkIsb7dlkmJT/hQ==", "requires": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -9546,15 +10003,15 @@ } }, "@uiw/react-codemirror": { - "version": "4.19.9", - "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.19.9.tgz", - "integrity": "sha512-U+A1fSfELMFFs5a+ZOPwCJKZMYaMy6QHOfNOOV7WhSveM7GYHT970GjTfs2dn1LlvhebwyK9ei0rCXZdsI9n9Q==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.0.tgz", + "integrity": "sha512-MnqTXfgeLA3fsUUQjqjJgemEuNyoGALgsExVm0NQAllAAi1wfj+IoKFeK+h3XXMlTFRCFYOUh4AHDv0YXJLsOg==", "requires": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.19.9", + "@uiw/codemirror-extensions-basic-setup": "4.23.0", "codemirror": "^6.0.0" } }, @@ -9565,9 +10022,9 @@ "dev": true }, "@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "requires": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -9587,9 +10044,9 @@ "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "@webassemblyjs/helper-numbers": { @@ -9610,15 +10067,15 @@ "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "@webassemblyjs/ieee754": { @@ -9646,28 +10103,28 @@ "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -9675,24 +10132,24 @@ } }, "@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -9701,12 +10158,12 @@ } }, "@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -9749,10 +10206,10 @@ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true }, - "acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "requires": {} }, @@ -9842,83 +10299,98 @@ "dev": true }, "array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" } }, "array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, + "array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + } + }, "array.prototype.flatmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", - "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" } }, "array.prototype.tosorted": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", - "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.1.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" } }, "arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "requires": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" } }, - "asynciterator.prototype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", - "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", + "attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "requires": { - "has-symbols": "^1.0.3" + "possible-typed-array-names": "^1.0.0" } }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true - }, "babel-loader": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", @@ -9940,33 +10412,33 @@ } }, "babel-plugin-polyfill-corejs2": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", - "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", "dev": true, "requires": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.3", + "@babel/helper-define-polyfill-provider": "^0.6.2", "semver": "^6.3.1" } }, "babel-plugin-polyfill-corejs3": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", - "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.3", - "core-js-compat": "^3.33.1" + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", - "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.4.3" + "@babel/helper-define-polyfill-provider": "^0.6.2" } }, "balanced-match": { @@ -10003,23 +10475,24 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "devOptional": true, "requires": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" } }, "buffer-from": { @@ -10029,14 +10502,16 @@ "dev": true }, "call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" } }, "callsites": { @@ -10045,9 +10520,10 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "caniuse-lite": { - "version": "1.0.30001564", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", - "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==" + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "devOptional": true }, "chalk": { "version": "2.4.2", @@ -10082,9 +10558,9 @@ "dev": true }, "classnames": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", - "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" }, "clone-deep": { "version": "4.0.1", @@ -10097,6 +10573,11 @@ "shallow-clone": "^3.0.0" } }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, "codemirror": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", @@ -10157,17 +10638,17 @@ } }, "copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", "dev": true, "requires": { - "fast-glob": "^3.2.11", + "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", - "globby": "^13.1.1", + "globby": "^14.0.0", "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" }, "dependencies": { "glob-parent": { @@ -10187,12 +10668,12 @@ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==" }, "core-js-compat": { - "version": "3.33.3", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", - "integrity": "sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==", + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.0.tgz", + "integrity": "sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==", "dev": true, "requires": { - "browserslist": "^4.22.1" + "browserslist": "^4.23.3" } }, "cosmiconfig": { @@ -10224,34 +10705,21 @@ } }, "css-loader": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.1.tgz", + "integrity": "sha512-OxIR5P2mjO1PSXk44bWuQ8XtMK4dpEqpIyERCx3ewOo3I8EmbcxMPUc5ScLtQfgXtOojoMv57So4V/C02HQLsw==", "dev": true, "requires": { "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" }, "dependencies": { - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -10274,6 +10742,44 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==" }, + "data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -10294,14 +10800,14 @@ "dev": true }, "define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "requires": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, "define-properties": { @@ -10315,14 +10821,10 @@ "object-keys": "^1.1.1" } }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" }, "dnd-core": { "version": "16.0.1", @@ -10352,9 +10854,10 @@ } }, "electron-to-chromium": { - "version": "1.4.592", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.592.tgz", - "integrity": "sha512-D3NOkROIlF+d5ixnz7pAf3Lu/AuWpd6AYgI9O67GQXMXTcCP1gJQRotOq35eQy5Sb4hez33XH1YdTtILA7Udww==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz", + "integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==", + "devOptional": true }, "emojis-list": { "version": "3.0.0", @@ -10363,9 +10866,9 @@ "dev": true }, "enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -10387,72 +10890,94 @@ } }, "es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "requires": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" + } + }, + "es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4" } }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, "es-iterator-helpers": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", - "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", "dev": true, "requires": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", + "internal-slot": "^1.0.7", "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" + "safe-array-concat": "^1.1.2" } }, "es-module-lexer": { @@ -10461,24 +10986,33 @@ "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", "dev": true }, + "es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0" + } + }, "es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "requires": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" } }, "es-shim-unscopables": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", - "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "dev": true, "requires": { - "has": "^1.0.3" + "hasown": "^2.0.0" } }, "es-to-primitive": { @@ -10493,9 +11027,10 @@ } }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "devOptional": true }, "escape-string-regexp": { "version": "1.0.5", @@ -10503,15 +11038,15 @@ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, "eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -10677,27 +11212,29 @@ } }, "eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "version": "7.35.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", + "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", "dev": true, "requires": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "dependencies": { "doctrine": { @@ -10716,12 +11253,12 @@ "dev": true }, "resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -10884,10 +11421,18 @@ } } }, + "file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "requires": { + "tslib": "^2.4.0" + } + }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -11059,14 +11604,16 @@ "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "devOptional": true }, "get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "requires": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", @@ -11074,13 +11621,14 @@ } }, "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" } }, "glob": { @@ -11127,22 +11675,29 @@ } }, "globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", "dev": true, "requires": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "dependencies": { + "path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true + }, "slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true } } @@ -11168,14 +11723,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -11188,18 +11735,18 @@ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "requires": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" } }, "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true }, "has-symbols": { @@ -11209,19 +11756,18 @@ "dev": true }, "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "requires": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" } }, "hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "requires": { "function-bind": "^1.1.2" } @@ -11302,13 +11848,13 @@ "dev": true }, "internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "requires": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "hasown": "^2.0.0", "side-channel": "^1.0.4" } }, @@ -11327,14 +11873,13 @@ } }, "is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "requires": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" } }, "is-arrayish": { @@ -11386,11 +11931,20 @@ "dev": true }, "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "requires": { + "hasown": "^2.0.0" + } + }, + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, "requires": { - "has": "^1.0.3" + "is-typed-array": "^1.1.13" } }, "is-date-object": { @@ -11436,15 +11990,15 @@ } }, "is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true }, "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true }, "is-number": { @@ -11488,18 +12042,18 @@ } }, "is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true }, "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "requires": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" } }, "is-string": { @@ -11521,18 +12075,18 @@ } }, "is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "requires": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" } }, "is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true }, "is-weakref": { @@ -11545,13 +12099,13 @@ } }, "is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" } }, "isarray": { @@ -11614,14 +12168,14 @@ } }, "jquery": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", - "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, "js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==" }, "js-tokens": { "version": "4.0.0", @@ -11662,7 +12216,8 @@ "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "devOptional": true }, "jsx-ast-utils": { "version": "3.3.3", @@ -11685,12 +12240,6 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "klona": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", - "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", - "dev": true - }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -11827,37 +12376,13 @@ } }, "mini-css-extract-plugin": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", - "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", "dev": true, "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "webpack-sources": "^1.1.0" - }, - "dependencies": { - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" } }, "minimatch": { @@ -11875,9 +12400,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true }, "natural-compare": { @@ -11893,9 +12418,10 @@ "dev": true }, "node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "devOptional": true }, "normalize-path": { "version": "3.0.0", @@ -11921,58 +12447,49 @@ "dev": true }, "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" } }, "object.entries": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", - "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "object.fromentries": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", - "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.hasown": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", - "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "requires": { - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" } }, "object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "once": { @@ -12070,9 +12587,9 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "picomatch": { "version": "2.3.1", @@ -12100,28 +12617,34 @@ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==" }, + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true + }, "postcss": { - "version": "8.4.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", - "integrity": "sha512-Wi8mWhncLJm11GATDaQKobXSNEYGUHeQLiQqDFG1qQ5UTDPTEvKw0Xt5NsTpktGTwLps3ByrWsBrG0rB8YQ9oA==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, "requires": {} }, "postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", "dev": true, "requires": { "icss-utils": "^5.0.0", @@ -12130,9 +12653,9 @@ } }, "postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", "dev": true, "requires": { "postcss-selector-parser": "^6.0.4" @@ -12148,9 +12671,9 @@ } }, "postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dev": true, "requires": { "cssesc": "^3.0.0", @@ -12210,9 +12733,9 @@ } }, "react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "requires": { "loose-envify": "^1.1.0" } @@ -12246,6 +12769,30 @@ } } }, + "react-datepicker": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.3.0.tgz", + "integrity": "sha512-EqRKLAtLZUTztiq6a+tjSjQX9ES0Xd229JPckAtyZZ4GoY3rtvNWAzkYZnQUf6zTWT50Ki0+t+W9VRQIkSJLfg==", + "requires": { + "@floating-ui/react": "^0.26.2", + "clsx": "^2.1.0", + "date-fns": "^3.3.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0" + } + }, + "react-diff-viewer-continued": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz", + "integrity": "sha512-kMZmUyb3Pv5L9vUtCfIGYsdOHs8mUojblGy1U1Sm0D7FhAOEsH9QhnngEIRo5hXWIPNGupNRJls1TJ6Eqx84eg==", + "requires": { + "@emotion/css": "^11.11.2", + "classnames": "^2.3.2", + "diff": "^5.1.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.8.1" + } + }, "react-dnd": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", @@ -12267,12 +12814,22 @@ } }, "react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "requires": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" + } + }, + "react-dropzone": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", + "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", + "requires": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" } }, "react-is": { @@ -12285,6 +12842,12 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-onclickoutside": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.1.tgz", + "integrity": "sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==", + "requires": {} + }, "react-overlays": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.9.3.tgz", @@ -12327,29 +12890,29 @@ } }, "react-redux": { - "version": "7.2.8", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.8.tgz", - "integrity": "sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "requires": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", + "@babel/runtime": "^7.12.1", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/use-sync-external-store": "^0.0.3", "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" + "react-is": "^18.0.0", + "use-sync-external-store": "^1.0.0" }, "dependencies": { "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" } } }, "react-select": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.0.tgz", - "integrity": "sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", "requires": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", @@ -12436,15 +12999,16 @@ "requires": {} }, "reflect.getprototypeof": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", - "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", "which-builtin-type": "^1.1.3" } @@ -12479,14 +13043,15 @@ } }, "regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" } }, "regexpu-core": { @@ -12576,13 +13141,13 @@ } }, "safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" } @@ -12593,20 +13158,20 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" } }, "sass": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", - "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "version": "1.77.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz", + "integrity": "sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -12615,19 +13180,18 @@ } }, "sass-loader": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-11.1.1.tgz", - "integrity": "sha512-fOCp/zLmj1V1WHDZbUbPgrZhA7HKXHEqkslzB+05U5K9SbSbcmH91C7QLW31AsXikxUMaxXRhhcqWZAxUMLDyA==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", + "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", "dev": true, "requires": { - "klona": "^2.0.4", "neo-async": "^2.6.2" } }, "scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "requires": { "loose-envify": "^1.1.0" } @@ -12676,38 +13240,42 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true }, "serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "requires": { "randombytes": "^2.1.0" } }, "set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dev": true, "requires": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.1" } }, "set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "requires": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" } }, "shallow-clone": { @@ -12735,14 +13303,15 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "slash": { @@ -12751,12 +13320,6 @@ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12780,52 +13343,67 @@ } }, "string.prototype.matchall": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", - "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4" + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + } + }, + "string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" } }, "string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "strip-ansi": { @@ -12844,14 +13422,14 @@ "dev": true }, "style-mod": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", - "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" }, "stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "supports-color": { "version": "5.5.0", @@ -12866,6 +13444,11 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -12873,9 +13456,9 @@ "dev": true }, "terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", @@ -12893,16 +13476,16 @@ } }, "terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "dependencies": { "schema-utils": { @@ -12938,6 +13521,11 @@ "is-number": "^7.0.0" } }, + "tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12954,50 +13542,55 @@ "dev": true }, "typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" } }, "typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "requires": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" } }, "typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" } }, "typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "requires": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" } }, "unbox-primitive": { @@ -13051,13 +13644,20 @@ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true }, + "unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true + }, "update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "devOptional": true, "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" } }, "uri-js": { @@ -13075,6 +13675,12 @@ "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", "requires": {} }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -13095,9 +13701,9 @@ } }, "watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, "requires": { "glob-to-regexp": "^0.4.1", @@ -13105,34 +13711,34 @@ } }, "webpack": { - "version": "5.89.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", - "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.0", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", + "enhanced-resolve": "^5.17.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.7", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "dependencies": { @@ -13175,18 +13781,29 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true + }, + "webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + } } } }, "webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, "requires": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" } }, "webpack-sources": { @@ -13218,13 +13835,13 @@ } }, "which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", "dev": true, "requires": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.0.5", "is-finalizationregistry": "^1.0.2", @@ -13233,39 +13850,39 @@ "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" } }, "which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "requires": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" } }, "which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" } }, "wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", - "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, "wrappy": { diff --git a/package.json b/package.json index c9ae26a0b1..d2e79d2b81 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "rdmo", "scripts": { - "build:prod": "webpack --config webpack/prod.config.js --mode production", - "build": "webpack --config webpack/dev.config.js --mode development", - "watch": "webpack --config webpack/dev.config.js --mode development --watch" + "build:prod": "webpack --config webpack.config.js --mode production", + "build": "webpack --config webpack.config.js --mode development", + "watch": "webpack --config webpack.config.js --mode development --watch" }, "author": "RDMO Arbeitsgemeinschaft ", "license": "Apache-2.0", @@ -13,43 +13,47 @@ }, "dependencies": { "@codemirror/lang-html": "^6.4.2", - "@codemirror/lang-javascript": "^6.1.4", - "@uiw/react-codemirror": "^4.19.9", + "@codemirror/lang-javascript": "^6.2.2", + "@uiw/react-codemirror": "^4.23.0", "bootstrap-sass": "^3.4.1", - "classnames": "^2.3.2", + "classnames": "^2.5.1", + "date-fns": "^3.6.0", "font-awesome": "4.7.0", - "jquery": "^3.6.0", - "js-cookie": "^2.2.1", + "jquery": "^3.7.1", + "js-cookie": "^3.0.5", "lodash": "^4.17.21", "popper.js": "^1.16.1", "prop-types": "^15.7.2", - "react": "^18.2.0", + "react": "^18.3.1", "react-bootstrap": "0.33.1", + "react-datepicker": "7.3.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", - "react-dom": "^18.2.0", - "react-redux": "^7.2.4", - "react-select": "^5.7.0", + "react-dom": "^18.3.1", + "react-dropzone": "^14.2.3", + "react-redux": "^8.1.3", + "react-select": "^5.8.0", "redux": "^4.1.1", "redux-logger": "^3.0.6", - "redux-thunk": "^2.3.0" + "redux-thunk": "^2.3.0", + "react-diff-viewer-continued": "^3.4.0" }, "devDependencies": { - "@babel/cli": "^7.23.4", - "@babel/core": "^7.23.3", - "@babel/preset-env": "^7.23.3", - "@babel/preset-react": "^7.23.3", + "@babel/cli": "^7.24.1", + "@babel/core": "^7.24.0", + "@babel/preset-env": "^7.25.3", + "@babel/preset-react": "^7.24.1", "babel-loader": "^9.1.3", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^5.2.0", - "eslint": "^8.54.0", - "eslint-plugin-react": "^7.33.2", + "copy-webpack-plugin": "^12.0.2", + "css-loader": "^7.1.1", + "eslint": "~8.56.0", + "eslint-plugin-react": "^7.35.0", "file-loader": "^6.2.0", - "mini-css-extract-plugin": "^1.4.0", - "sass": "^1.55.0", - "sass-loader": "^11.0.1", - "webpack": "^5.89.0", + "mini-css-extract-plugin": "^2.9.0", + "sass": "^1.77.1", + "sass-loader": "^16.0.0", + "webpack": "^5.93.0", "webpack-cli": "^5.1.4", - "webpack-merge": "5.10.0" + "webpack-merge": "6.0.1" } } diff --git a/pyproject.toml b/pyproject.toml index e9f6be43d5..9df7193c84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,68 +40,68 @@ dependencies = [ # dependencies with major version on zero are declared with # major.minor.patch, because they can potentially introduce breaking changes # in minor version updates anytime - "defusedcsv~=2.0", - "defusedxml~=0.7.1", - "django~=4.2", - "django-cleanup~=8.0", - "django-compressor~=4.4", - "django-extensions~=3.2", - "django-filter~=23.2", - "django-libsass~=0.9", - "django-mathfilters~=1.0", - "django-mptt==0.14.0", # pinned, 0.15 requires Python >= 3.9 - "django-rest-swagger~=2.2", - "django-settings-export~=1.2", - "django-split-settings~=1.2", - "django-widget-tweaks~=1.5", - "djangorestframework~=3.14", - "drf-extensions~=0.7.1", - "iso8601~=2.0", - "markdown~=3.4", - "packaging~=23.2", - "pypandoc~=1.11", - "requests-toolbelt~=1.0", - "rules~=3.3", + "defusedcsv>=2.0,<3.0", + "defusedxml>=0.7.1,<1.0", + "django>=4.2,<5.0", + "django-cleanup>=8.0,<9.0", + "django-compressor>=4.4,<5.0", + "django-extensions>=3.2,<4.0", + "django-filter>=23.2,<25.0", + "django-libsass>=0.9,<1.0", + "django-mathfilters>=1.0,<2.0", + "django-mptt==0.14.0", # pinned, 0.15 requires Python >= 3.9 + "django-rest-swagger>=2.2,<3.0", + "django-settings-export>=1.2,<2.0", + "django-split-settings>=1.2,<2.0", + "django-widget-tweaks>=1.5,<2.0", + "djangorestframework>=3.15,<4.0", + "drf-extensions>=0.7.1,<1.0", + "iso8601>=2.0,<3.0", + "markdown>=3.4,<4.0", + "packaging>=23.2,<25.0", + "pypandoc>=1.11,<2.0", + "requests-toolbelt>=1.0,<2.0", + "rules>=3.4,<4.0", ] [project.optional-dependencies] allauth = [ - "django-allauth~=0.57.0", + "django-allauth[socialaccount,openid]>=64.1.0,<64.2.0", ] ci = [ - "coveralls~=3.3", "rdmo[dev]", + "pytest-github-actions-annotate-failures>=0.2.0,<0.3.0", ] dev = [ - "build~=1.0", - "pipdeptree~=2.13", - "pre-commit~=3.4", - "setuptools~=69.0", - "twine~=4.0", - "wheel~=0.42.0", + "build>=1.0,<2.0", + "pipdeptree>=2.13,<3.0", + "pre-commit>=3.4,<4.0", + "setuptools>=73,<74", + "twine>=5.1.1,<6.0", + "wheel>=0.42,<0.45", "rdmo[allauth]", "rdmo[pytest]", ] gunicorn = [ - "gunicorn~=21.2", + "gunicorn>=23.0,<24.0", ] ldap = [ - "django-auth-ldap~=4.5", + "django-auth-ldap>=4.5,<5.0", ] mysql = [ - "mysqlclient~=2.2", + "mysqlclient>=2.2,<3.0", ] postgres = [ - "psycopg[binary]~=3.1", + "psycopg[binary]>=3.1,<4.0", ] pytest = [ - "pytest~=7.4", - "pytest-cov~=4.1", - "pytest-django~=4.5", - "pytest-mock~=3.11", - "pytest-playwright~=0.4.3", - "pytest-randomly~=3.15", - "pytest-xdist~=3.3", + "pytest>=8.0,<9.0", + "pytest-cov>=4.1,<6.0", + "pytest-django>=4.5,<5.0", + "pytest-mock>=3.11,<4.0", + "pytest-playwright>=0.4.3,<0.6.0", + "pytest-randomly>=3.15,<4.0", + "pytest-xdist>=3.3,<4.0", ] [project.urls] @@ -123,8 +123,9 @@ exclude = ["*assets*", "*tests*"] version = {attr = "rdmo.__version__"} [tool.ruff] -target-version = "py38" line-length = 120 + +[tool.ruff.lint] select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions @@ -133,10 +134,13 @@ select = [ "F", # pyflakes "I", # isort "PGH", # pygrep-hooks + "PT", # flake8-pytest-style "RUF", # ruff "UP", # pyupgrade "W", # pycodestyle "YTT", # flake8-2020 + "G", # flake8-logging-format + "INT" # flake8-gettext ] ignore = [ "B006", # mutable-argument-default @@ -145,7 +149,7 @@ ignore = [ "RUF012", # mutable-class-default ] -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["rdmo"] section-order = [ "future", @@ -158,12 +162,12 @@ section-order = [ "local-folder" ] -[tool.ruff.isort.sections] +[tool.ruff.lint.isort.sections] pytest = ["pytest"] django = ["django"] rest_framework = ["rest_framework"] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "rdmo/**/models/__init__.py" = [ "F401", # unused-import ] @@ -184,10 +188,13 @@ rest_framework = ["rest_framework"] "F821", # undefined-names ] +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +parametrize-names-type = "csv" + [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings" -testpaths = ["rdmo"] -python_files = "test_*.py" +testpaths = ["rdmo/*/tests"] pythonpath = [".", "testing"] addopts = '-p no:randomly -m "not e2e"' markers = [ @@ -201,6 +208,12 @@ filterwarnings = [ "ignore:'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning", "ignore:pkg_resources is deprecated as an API:DeprecationWarning", + # ignore warning from rest_framework about coreapi + "ignore:CoreAPI compatibility is deprecated and will be removed in DRF 3.17:rest_framework.RemovedInDRF317Warning", + + # ignore warning from django_filters + "ignore:Built-in schema generation is deprecated. Use drf-spectacular.:django_filters.RemovedInDjangoFilter25Warning", + # ignore warnings raised from within django itself # django/core/files/storage/__init__.py "ignore:django.core.files.storage.get_storage_class is deprecated:django.utils.deprecation.RemovedInDjango51Warning", @@ -221,3 +234,10 @@ exclude_lines = [ "raise Exception", "except ImportError:" ] + +[tool.typos] # Ref: https://github.com/crate-ci/typos/blob/master/docs/reference.md +# add "spellchecker:disable-line" to ignore specific lines +default.extend-ignore-re = [ + "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # for .py files + "(?Rm)^.*$", # for .html files +] diff --git a/rdmo/__init__.py b/rdmo/__init__.py index e835b9d023..8a124bf648 100644 --- a/rdmo/__init__.py +++ b/rdmo/__init__.py @@ -1 +1 @@ -__version__ = "2.1.3" +__version__ = "2.2.0" diff --git a/rdmo/accounts/admin.py b/rdmo/accounts/admin.py index 1cfede8540..8d65fb1f57 100644 --- a/rdmo/accounts/admin.py +++ b/rdmo/accounts/admin.py @@ -30,15 +30,29 @@ class RoleAdmin(admin.ModelAdmin): list_display = ('user', 'email', 'members', 'managers', 'editors', 'reviewers') + readonly_fields = ('user', ) + def get_queryset(self, request): return Role.objects.prefetch_related( 'member', 'manager', 'editor', 'reviewer').annotate( - Count('member'), Count('manager'), Count('editor'), Count('reviewer'), + Count('member', distinct=True), + Count('manager', distinct=True), + Count('editor', distinct=True), + Count('reviewer', distinct=True), sites_count=Value(Site.objects.count()) ) + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + form.base_fields['member'].widget.can_add_related = False + form.base_fields['manager'].widget.can_add_related = False + form.base_fields['editor'].widget.can_add_related = False + form.base_fields['reviewer'].widget.can_add_related = False + + return form + @staticmethod - def render_all_sites_or_join(obj, field_name: str) -> str: + def get_sites_for_role(obj, field_name: str) -> str: if getattr(obj, f'{field_name}__count', 0) == obj.sites_count: return 'all Sites' return ', '.join([site.domain for site in getattr(obj, field_name).all()]) @@ -47,13 +61,13 @@ def email(self, obj): return obj.user.email def members(self, obj): - return self.render_all_sites_or_join(obj, 'member') + return self.get_sites_for_role(obj, 'member') def managers(self, obj): - return self.render_all_sites_or_join(obj, 'manager') + return self.get_sites_for_role(obj, 'manager') def editors(self, obj): - return self.render_all_sites_or_join(obj, 'editor') + return self.get_sites_for_role(obj, 'editor') def reviewers(self, obj): - return self.render_all_sites_or_join(obj, 'reviewer') + return self.get_sites_for_role(obj, 'reviewer') diff --git a/rdmo/accounts/forms.py b/rdmo/accounts/forms.py index e82f7f4c38..fe75e98b4b 100644 --- a/rdmo/accounts/forms.py +++ b/rdmo/accounts/forms.py @@ -5,6 +5,9 @@ from django.contrib.auth import get_user_model from django.utils.translation import gettext_lazy as _ +from allauth.account.forms import LoginForm as AllauthLoginForm +from allauth.account.forms import SignupForm as AllauthSignupForm + from .models import AdditionalField, AdditionalFieldValue, ConsentFieldValue log = logging.getLogger(__name__) @@ -48,7 +51,7 @@ def __init__(self, *args, **kwargs): # existing user is going to be updated if self.instance.pk is not None: for additional_field_value in AdditionalFieldValue.objects.filter(user=self.instance): - self.fields[additional_field.key].initial = additional_field_value.value + self.fields[additional_field_value.field.key].initial = additional_field_value.value def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -68,7 +71,18 @@ def _save_additional_values(self, user=None): additional_value.save() -class SignupForm(ProfileForm): +class LoginForm(AllauthLoginForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # remove forget password link introduced with allauth 0.57.0 + password_field = self.fields.get('password') + if password_field: + password_field.help_text = None + + +class SignupForm(AllauthSignupForm, ProfileForm): use_required_attribute = False diff --git a/rdmo/accounts/serializers/v1.py b/rdmo/accounts/serializers/v1.py index cd7018c4a9..eeb6f1ddc5 100644 --- a/rdmo/accounts/serializers/v1.py +++ b/rdmo/accounts/serializers/v1.py @@ -72,7 +72,9 @@ class Meta: 'id', 'groups', 'role', - 'memberships' + 'memberships', + 'is_superuser', + 'is_staff' ] if settings.USER_API: fields += [ diff --git a/rdmo/accounts/static/accounts/img/orcid_16x16.png b/rdmo/accounts/static/accounts/img/orcid_16x16.png new file mode 100644 index 0000000000..6b697e4d7b Binary files /dev/null and b/rdmo/accounts/static/accounts/img/orcid_16x16.png differ diff --git a/rdmo/accounts/tests/test_views.py b/rdmo/accounts/tests/test_views.py index 955a68c4b2..a76ce39052 100644 --- a/rdmo/accounts/tests/test_views.py +++ b/rdmo/accounts/tests/test_views.py @@ -38,7 +38,7 @@ @pytest.fixture(autouse=True, scope='module') -def reload_urls_at_teardown(): +def _reload_urls_at_teardown(): '''Clear the url cache after the test function.''' yield reload_urls('accounts') diff --git a/rdmo/accounts/views.py b/rdmo/accounts/views.py index 7ed194997a..3300904cb3 100644 --- a/rdmo/accounts/views.py +++ b/rdmo/accounts/views.py @@ -48,7 +48,7 @@ def remove_user(request): log.info('Remove user form is disabled in settings PROFILE_DELETE') return render(request, 'profile/profile_remove_closed.html') form = RemoveForm(request.POST or None, request=request) - log.debug('Remove user form initialized for "%s"' % request.user.username) + log.debug('Remove user form initialized for "%s"', request.user.username) if request.method == 'POST': if 'cancel' in request.POST: diff --git a/rdmo/accounts/viewsets.py b/rdmo/accounts/viewsets.py index b377dd08df..09604f146f 100644 --- a/rdmo/accounts/viewsets.py +++ b/rdmo/accounts/viewsets.py @@ -1,5 +1,8 @@ from django.contrib.auth import get_user_model +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet from django_filters.rest_framework import DjangoFilterBackend @@ -40,3 +43,8 @@ def get_queryset(self): 'role__member', 'role__manager', 'role__editor', 'role__reviewer', 'memberships') + + @action(detail=False, permission_classes=(IsAuthenticated, )) + def current(self, request): + serializer = UserSerializer(request.user) + return Response(serializer.data) diff --git a/rdmo/conditions/imports.py b/rdmo/conditions/imports.py index f9f5f513f1..3a786a1506 100644 --- a/rdmo/conditions/imports.py +++ b/rdmo/conditions/imports.py @@ -1,42 +1,14 @@ -import logging - -from django.contrib.sites.models import Site - -from rdmo.core.imports import check_permissions, set_common_fields, set_foreign_field, validate_instance +from rdmo.core.import_helpers import ElementImportHelper, ExtraFieldHelper from .models import Condition from .validators import ConditionLockedValidator, ConditionUniqueURIValidator -logger = logging.getLogger(__name__) - - -def import_condition(element, save=False, user=None): - try: - condition = Condition.objects.get(uri=element.get('uri')) - except Condition.DoesNotExist: - condition = Condition() - - set_common_fields(condition, element) - - set_foreign_field(condition, 'source', element) - set_foreign_field(condition, 'target_option', element) - - condition.relation = element.get('relation') - condition.target_text = element.get('target_text') or '' - - validate_instance(condition, element, ConditionLockedValidator, ConditionUniqueURIValidator) - - check_permissions(condition, element, user) - - if save and not element.get('errors'): - if condition.id: - element['updated'] = True - logger.info('Condition %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('Condition created with uri %s.', element.get('uri')) - - condition.save() - condition.editors.add(Site.objects.get_current()) - - return condition +import_helper_condition = ElementImportHelper( + model=Condition, + validators=(ConditionLockedValidator, ConditionUniqueURIValidator), + foreign_fields=('source', 'target_option'), + extra_fields=( + ExtraFieldHelper(field_name='relation', value=''), + ExtraFieldHelper(field_name='target_text', value=''), + ), +) diff --git a/rdmo/conditions/tests/test_admin.py b/rdmo/conditions/tests/test_admin.py index d051b6d49e..65bc483aa6 100644 --- a/rdmo/conditions/tests/test_admin.py +++ b/rdmo/conditions/tests/test_admin.py @@ -1,9 +1,7 @@ from django.urls import reverse -def test_condition_search(db, client): - client.login(username='admin', password='admin') - +def test_condition_search(admin_client): url = reverse('admin:conditions_condition_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 diff --git a/rdmo/conditions/tests/test_viewset_condition_multisite.py b/rdmo/conditions/tests/test_viewset_condition_multisite.py index 0455e1c22a..6d1f0da587 100644 --- a/rdmo/conditions/tests/test_viewset_condition_multisite.py +++ b/rdmo/conditions/tests/test_viewset_condition_multisite.py @@ -4,9 +4,10 @@ from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import Condition from .test_viewset_condition import export_formats, urlnames diff --git a/rdmo/core/assets/js/components/FileUploadButton.js b/rdmo/core/assets/js/components/FileUploadButton.js new file mode 100644 index 0000000000..9837b634d9 --- /dev/null +++ b/rdmo/core/assets/js/components/FileUploadButton.js @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { useDropzone } from 'react-dropzone' + +const FileUploadButton = ({ acceptedTypes, buttonProps, buttonLabel, onImportFile }) => { + const { getRootProps, getInputProps } = useDropzone({ + accept: acceptedTypes, + onDrop: acceptedFiles => { + if (acceptedFiles.length > 0) { + onImportFile(acceptedFiles[0]) + } + } + }) + + return ( +
+ + +
+ ) +} + +FileUploadButton.propTypes = { + acceptedTypes: PropTypes.arrayOf(PropTypes.string), + buttonProps: PropTypes.object, + buttonLabel: PropTypes.string.isRequired, + onImportFile: PropTypes.func.isRequired, +} + +export default FileUploadButton diff --git a/rdmo/core/assets/js/components/Modal.js b/rdmo/core/assets/js/components/Modal.js new file mode 100644 index 0000000000..a3f2cd70b8 --- /dev/null +++ b/rdmo/core/assets/js/components/Modal.js @@ -0,0 +1,40 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Modal as BootstrapModal } from 'react-bootstrap' + +const Modal = ({ bsSize, buttonLabel, buttonProps, title, show, onClose, onSave, children }) => { + return ( + + +

{title}

+
+ + { children } + + + + { onSave ? + + : null + } + +
+ ) +} + +Modal.propTypes = { + bsSize: PropTypes.oneOf(['lg', 'large', 'sm', 'small']), + buttonLabel: PropTypes.string, + buttonProps: PropTypes.object, + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired, + onClose: PropTypes.func.isRequired, + onSave: PropTypes.func, + show: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, +} + +export default Modal diff --git a/rdmo/core/assets/js/components/SearchField.js b/rdmo/core/assets/js/components/SearchField.js new file mode 100644 index 0000000000..1201bc0dd8 --- /dev/null +++ b/rdmo/core/assets/js/components/SearchField.js @@ -0,0 +1,56 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const SearchField = ({ value, onChange, onSearch, placeholder }) => { + + const handleSearch = () => { + onSearch(value) + } + + const handleChange = (newValue) => { + onChange(newValue) + } + + const handleButtonClick = () => { + onChange('') + handleSearch() + } + + const handleKeyDown = (event) => { + if (event.key === 'Enter') { + handleSearch() + } + } + + return ( +
+
+ handleChange(e.target.value)} + onKeyDown={handleKeyDown} + /> + + + + +
+
+ ) +} + +SearchField.propTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + onSearch: PropTypes.func.isRequired, + placeholder: PropTypes.string.isRequired, +} + +export default SearchField diff --git a/rdmo/core/assets/js/components/Select.js b/rdmo/core/assets/js/components/Select.js new file mode 100644 index 0000000000..246b54b261 --- /dev/null +++ b/rdmo/core/assets/js/components/Select.js @@ -0,0 +1,38 @@ +import React from 'react' +import PropTypes from 'prop-types' +import ReactSelect from 'react-select' + +const Select = ({ options, onChange, placeholder, value }) => { + const selectedOption = options.find(option => option.value === value) || null + const handleChange = (selected) => { + onChange(selected ? selected.value : null) + } + + return ( +
+ +
+ ) +} + +Select.propTypes = { + value: PropTypes.string, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired + }) + ).isRequired, + onChange: PropTypes.func, + placeholder: PropTypes.string +} + +export default Select diff --git a/rdmo/core/assets/js/components/UploadDropZone.js b/rdmo/core/assets/js/components/UploadDropZone.js new file mode 100644 index 0000000000..d4f335f52b --- /dev/null +++ b/rdmo/core/assets/js/components/UploadDropZone.js @@ -0,0 +1,40 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { useDropzone } from 'react-dropzone' + +const UploadDropZone = ({ acceptedTypes, onImportFile }) => { + const [errorMessage, setErrorMessage] = useState('') + + const { getRootProps, getInputProps } = useDropzone({ + accept: acceptedTypes, + onDropAccepted: acceptedFiles => { + if (acceptedFiles.length > 0) { + onImportFile(acceptedFiles[0]) + setErrorMessage('') + } + }, + onDropRejected: rejectedFiles => { + console.log(rejectedFiles) + setErrorMessage(interpolate(gettext('%s has unsupported file type'), [rejectedFiles[0].path])) + } + }) + + return ( +
+
+ +

+ {gettext('Drag and drop a file here or click to select a file')} +

+ {errorMessage &&
{errorMessage}
} +
+
+ ) +} + +UploadDropZone.propTypes = { + acceptedTypes: PropTypes.arrayOf(PropTypes.string), + onImportFile: PropTypes.func.isRequired, +} + +export default UploadDropZone diff --git a/rdmo/core/assets/js/components/index.js b/rdmo/core/assets/js/components/index.js new file mode 100644 index 0000000000..b69b60e4cd --- /dev/null +++ b/rdmo/core/assets/js/components/index.js @@ -0,0 +1,7 @@ +export { default as FileUploadButton } from './FileUploadButton' +export { default as Link } from './Link' +export { default as LinkButton } from './LinkButton' +export { default as Modal } from './Modal' +export { default as UploadDropZone } from './UploadDropZone' +export { default as SearchField } from './SearchField' +export { default as Select } from './Select' diff --git a/rdmo/core/assets/js/hooks/index.js b/rdmo/core/assets/js/hooks/index.js new file mode 100644 index 0000000000..35876244a8 --- /dev/null +++ b/rdmo/core/assets/js/hooks/index.js @@ -0,0 +1,3 @@ +export { default as useFormattedDateTime } from './useFormattedDateTime' +export { default as useModal } from './useModal' +export { default as useScrollToTop } from './useScrollToTop' diff --git a/rdmo/core/assets/js/hooks/useFormattedDateTime.js b/rdmo/core/assets/js/hooks/useFormattedDateTime.js new file mode 100644 index 0000000000..4986a4dfcf --- /dev/null +++ b/rdmo/core/assets/js/hooks/useFormattedDateTime.js @@ -0,0 +1,20 @@ +import { format } from 'date-fns' +import { de, enUS } from 'date-fns/locale' + +const getLocaleObject = (language) => { + return language === 'de' ? de : enUS +} + +const FORMAT_STRINGS = { + en: 'MMM d, yyyy, h:mm a', + de: 'd. MMM yyyy, H:mm', +} + +export const useFormattedDateTime = (date, language) => { + const locale = getLocaleObject(language) + const formatString = language === 'de' ? FORMAT_STRINGS.de : FORMAT_STRINGS.en + + return format(new Date(date), formatString, { locale }) +} + +export default useFormattedDateTime diff --git a/rdmo/core/assets/js/hooks/useModal.js b/rdmo/core/assets/js/hooks/useModal.js new file mode 100644 index 0000000000..4846d0c6fe --- /dev/null +++ b/rdmo/core/assets/js/hooks/useModal.js @@ -0,0 +1,11 @@ +import { useState } from 'react' + +const useModal = () => { + const [show, setShow] = useState(false) + const open = () => setShow(true) + const close = () => setShow(false) + + return {show, open, close} +} + +export default useModal diff --git a/rdmo/core/assets/js/hooks/useScrollToTop.js b/rdmo/core/assets/js/hooks/useScrollToTop.js new file mode 100644 index 0000000000..16e600b80e --- /dev/null +++ b/rdmo/core/assets/js/hooks/useScrollToTop.js @@ -0,0 +1,22 @@ +import { useState, useEffect } from 'react' + +const useScrollToTop = () => { + const [showTopButton, setShowTopButton] = useState(false) + + useEffect(() => { + const handleScroll = () => { + setShowTopButton(window.pageYOffset > 100) + } + + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, []) + + const scrollToTop = () => { + window.scrollTo({ top: 0, behavior: 'smooth' }) + } + + return { showTopButton, scrollToTop } +} + +export default useScrollToTop diff --git a/rdmo/core/assets/js/utils/api.js b/rdmo/core/assets/js/utils/api.js new file mode 100644 index 0000000000..2f7b3724ff --- /dev/null +++ b/rdmo/core/assets/js/utils/api.js @@ -0,0 +1,15 @@ +const encodeParams = params => { + return Object.entries(params).map(item => { + const [key, value] = item + + if (Array.isArray(value)) { + return value.map(v => { + return encodeURIComponent(key) + '=' + encodeURIComponent(v) + }).join('&') + } else { + return encodeURIComponent(key) + '=' + encodeURIComponent(value) + } + }).join('&') +} + +export { encodeParams } diff --git a/rdmo/core/assets/js/utils/index.js b/rdmo/core/assets/js/utils/index.js new file mode 100644 index 0000000000..c2d32da182 --- /dev/null +++ b/rdmo/core/assets/js/utils/index.js @@ -0,0 +1,5 @@ +export * from './api' +export { default as baseUrl } from './baseUrl' +export { default as language } from './language' +export { default as siteId } from './siteId' +export { default as staticUrl } from './staticUrl' diff --git a/rdmo/core/assets/js/utils/language.js b/rdmo/core/assets/js/utils/language.js new file mode 100644 index 0000000000..58dc8a369e --- /dev/null +++ b/rdmo/core/assets/js/utils/language.js @@ -0,0 +1,2 @@ +// take the language from the of the django template +export default document.querySelector('meta[name="language"]').content diff --git a/rdmo/core/assets/js/utils/siteId.js b/rdmo/core/assets/js/utils/siteId.js new file mode 100644 index 0000000000..7b413b672e --- /dev/null +++ b/rdmo/core/assets/js/utils/siteId.js @@ -0,0 +1,2 @@ +// take the site_id from the of the django template +export default Number(document.querySelector('meta[name="site_id"]').content) diff --git a/rdmo/core/assets/js/utils/staticUrl.js b/rdmo/core/assets/js/utils/staticUrl.js new file mode 100644 index 0000000000..0a1323cb10 --- /dev/null +++ b/rdmo/core/assets/js/utils/staticUrl.js @@ -0,0 +1,2 @@ +// take the staticurl from the of the django template +export default document.querySelector('meta[name="staticurl"]').content.replace(/\/+$/, '') diff --git a/rdmo/core/assets/scss/variables.scss b/rdmo/core/assets/scss/variables.scss index 1af6a6b451..c93eca130b 100644 --- a/rdmo/core/assets/scss/variables.scss +++ b/rdmo/core/assets/scss/variables.scss @@ -25,6 +25,7 @@ $footer-link-color: #999; $footer-background-color: #001; $danger-color: #a94442; +$warning-color: #8a6d3b; $success-color: #5cb85c; $modal-border-color: #e5e5e5; diff --git a/rdmo/core/constants.py b/rdmo/core/constants.py index 1e1cbc3a78..d2a92c5340 100644 --- a/rdmo/core/constants.py +++ b/rdmo/core/constants.py @@ -5,6 +5,7 @@ VALUE_TYPE_INTEGER = 'integer' VALUE_TYPE_FLOAT = 'float' VALUE_TYPE_BOOLEAN = 'boolean' +VALUE_TYPE_DATE = 'date' VALUE_TYPE_DATETIME = 'datetime' VALUE_TYPE_OPTIONS = 'option' VALUE_TYPE_EMAIL = 'email' @@ -16,6 +17,7 @@ (VALUE_TYPE_INTEGER, _('Integer')), (VALUE_TYPE_FLOAT, _('Float')), (VALUE_TYPE_BOOLEAN, _('Boolean')), + (VALUE_TYPE_DATE, _('Date')), (VALUE_TYPE_DATETIME, _('Datetime')), (VALUE_TYPE_EMAIL, _('E-mail')), (VALUE_TYPE_PHONE, _('Phone')), @@ -77,3 +79,17 @@ "tib": {"base": 1024, "power": 4}, "pib": {"base": 1024, "power": 5}, } + +RDMO_MODELS = { + 'attribute': 'domain.attribute', + 'condition': 'conditions.condition', + 'option': 'options.option', + 'optionset': 'options.optionset', + 'question': 'questions.question', + 'questionset': 'questions.questionset', + 'page': 'questions.page', + 'section': 'questions.section', + 'catalog': 'questions.catalog', + 'task': 'tasks.task', + 'view': 'views.view' +} diff --git a/rdmo/core/import_helpers.py b/rdmo/core/import_helpers.py new file mode 100644 index 0000000000..544db24971 --- /dev/null +++ b/rdmo/core/import_helpers.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass, field +from inspect import signature +from typing import Callable, Iterable, Optional, Sequence, Union + +from django.db import models + +ELEMENT_COMMON_FIELDS = ( + 'uri_prefix', + 'uri_path', + 'comment', +) + +@dataclass(frozen=True) +class ThroughInstanceMapper: + field_name: str + source_name: str + target_name: str + through_name: str + + +@dataclass(frozen=True) +class ExtraFieldHelper: + field_name: str + value: Union[str, bool, int, None] = None + callback: Optional[Callable] = None + overwrite_in_element: bool = False + + def get_value(self, **kwargs): + if self.value is not None: + return self.value + elif self.callback is not None: + return self.get_value_from_callback(self.callback, kwargs) + + @staticmethod + def get_value_from_callback(callback, kwargs): + if not callable(callback): + raise TypeError('callback must be callable') + sig = signature(callback) + kwargs = {k: val for k, val in kwargs.items() if k in sig.parameters} + value = callback(**kwargs) + return value + + +@dataclass(frozen=True) +class ElementImportHelper: + model: Optional[models.Model] = field(default=None) + model_path: Optional[str] = field(default=None) + validators: Iterable[Callable] = field(default_factory=list) + common_fields: Sequence[str] = field(default=ELEMENT_COMMON_FIELDS) + lang_fields: Sequence[str] = field(default_factory=list) + foreign_fields: Sequence[str] = field(default_factory=list) + extra_fields: Sequence[ExtraFieldHelper] = field(default_factory=list) + m2m_instance_fields: Sequence[str] = field(default_factory=list) + m2m_through_instance_fields: Sequence[ThroughInstanceMapper] = field(default_factory=list) + reverse_m2m_through_instance_fields: Sequence[ThroughInstanceMapper] = field(default_factory=list) + add_current_site_editors: bool = field(default=True) + add_current_site_sites: bool = field(default=False) diff --git a/rdmo/core/imports.py b/rdmo/core/imports.py index 587afd54e1..7fd28969c4 100644 --- a/rdmo/core/imports.py +++ b/rdmo/core/imports.py @@ -1,19 +1,37 @@ import logging import tempfile import time +from collections import defaultdict +from enum import Enum from os.path import join as pj -from pathlib import Path from random import randint +from typing import List, Optional, Tuple, Union from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ from rest_framework.utils import model_meta +from rdmo.core.constants import RDMO_MODELS +from rdmo.core.import_helpers import ExtraFieldHelper from rdmo.core.utils import get_languages +from rdmo.core.validators import LockedValidator logger = logging.getLogger(__name__) +class ImportElementFields(str, Enum): + DIFF = "updated_and_changed" + NEW = "new_data" + CURRENT = "current_data" + WARNINGS = "warnings" + ERRORS = "errors" + UPDATED = "updated" + CREATED = "created" + CHANGED_FIELDS = "changedFields" # for ignored_keys when ordering at save + + def handle_uploaded_file(filedata): tempfilename = generate_tempfile_name() with open(tempfilename, 'wb+') as destination: @@ -29,10 +47,6 @@ def handle_fetched_file(filedata): return tempfilename -def file_path_exists(file_path): - return Path(file_path).exists() - - def generate_tempfile_name(): t = int(round(time.time() * 1000)) r = randint(10000, 99999) @@ -40,23 +54,155 @@ def generate_tempfile_name(): return fn -def set_common_fields(instance, element): - instance.uri_prefix = element.get('uri_prefix') or '' - instance.uri_path = element.get('uri_path') or '' - instance.key = element.get('key') or '' - instance.comment = element.get('comment') or '' +def get_or_return_instance(model: models.Model, uri: Optional[str] = None) -> Tuple[models.Model, bool]: + if uri is None: + return model(), True + try: + return model.objects.get(uri=uri), False + except model.DoesNotExist: + return model(), True + except model.MultipleObjectsReturned: + return model.objects.filter(uri=uri).first(), False + + +def get_rdmo_model_path(target_name: str, field_name: str): + try: + return RDMO_MODELS[target_name] + except KeyError: + if 'parent' in target_name and 'questionset' in field_name: + return RDMO_MODELS[field_name] + + +def make_import_info_msg(verbose_name: str, created: bool, uri: Optional[str] = None): + if uri is None: + return f"{verbose_name}, no uri" + if created: + return f"{verbose_name} created with {uri}" + return f"{verbose_name} {uri} updated" + + +def _initialize_tracking_field(element: dict, element_field: str): + if element[ImportElementFields.DIFF].get(element_field) is None: + element[ImportElementFields.DIFF][element_field] = { + ImportElementFields.ERRORS: [], + ImportElementFields.WARNINGS: defaultdict(list) + } + return + if ImportElementFields.ERRORS not in element[ImportElementFields.DIFF][element_field]: + element[ImportElementFields.DIFF][element_field][ImportElementFields.ERRORS] = [] + if ImportElementFields.WARNINGS not in element[ImportElementFields.DIFF][element_field]: + element[ImportElementFields.DIFF][element_field][ImportElementFields.WARNINGS] = defaultdict(list) + -def set_lang_field(instance, field_name, element): - for lang_code, lang_string, lang_field in get_languages(): - field = element.get(f'{field_name}_{lang_code}') - if field: - setattr(instance, f'{field_name}_{lang_field}', field) - else: - setattr(instance, f'{field_name}_{lang_field}', '') +def _append_warning(element: dict, element_field: str, warning: str): + element[ImportElementFields.DIFF][element_field][ImportElementFields.WARNINGS][element['uri']].append(warning) -def set_foreign_field(instance, field_name, element) -> None: +def _append_error(element: dict, element_field: str, error: str): + element[ImportElementFields.DIFF][element_field][ImportElementFields.ERRORS].append(error) + + +def track_messages_on_element(element: dict, + element_field: str, + warning: Optional[str] = None, + error: Optional[str] = None): + if warning is not None: + _initialize_tracking_field(element, element_field) + _append_warning(element, element_field, warning) + if error is not None: + _initialize_tracking_field(element, element_field) + _append_error(element, element_field, error) + + +def _initialize_track_changes_element_field(element: dict, element_field: str) -> None: + if ImportElementFields.DIFF not in element: + element[ImportElementFields.DIFF] = {} + + if element_field and element_field not in element[ImportElementFields.DIFF]: + element[ImportElementFields.DIFF][element_field] = {} + + +def track_changes_on_element(element: dict, + element_field: str, + new_value: Union[str, List[str], None] = None, + instance_field: Optional[str] = None, + original=None, + original_value: Optional[Union[str, List[str]]] = None): + if (original is None and original_value is None) or new_value is None: + return + + _initialize_track_changes_element_field(element, element_field) + + if original_value is None and original is not None: + lookup_field = element_field if instance_field is None else instance_field + original_value = getattr(original, lookup_field, '') + + element[ImportElementFields.DIFF][element_field][ImportElementFields.CURRENT] = original_value + element[ImportElementFields.DIFF][element_field][ImportElementFields.NEW] = new_value + + +def get_lang_field_values(field_name: str, + element: Optional[dict] = None, + instance: Optional[models.Model] = None): + if element is not None and instance is not None: + raise ValueError("Please choose one of each") + + ret = [] + for lang_code, lang_verbose_name, lang_field in get_languages(): + name_code = f'{field_name}_{lang_code}' + name_field = f'{field_name}_{lang_field}' + row = {} + row['element_key'] = name_code + row['instance_field'] = name_field + if element: + row['value'] = element.get(name_code, '') or '' + if instance: + row['value'] = getattr(instance, name_field, '') or '' + ret.append(row) + return ret + + +def set_common_fields(instance, field_name, element, original=None): + element_value = element.get(field_name) or '' + if field_name == 'comment' and original is not None: + # prevent overwrite with an empty comment when updating an element + original_value = getattr(original, field_name) + if original_value and not element_value: + element_value = original_value + element[field_name] = element_value + + setattr(instance, field_name, element_value) + # track changes for common fields + track_changes_on_element(element, field_name, new_value=element_value, original=original) + + +def set_lang_field(instance, field_name, element, original=None): + languages_field_values = get_lang_field_values(field_name, element=element) + for lang_fields_value in languages_field_values: + field_lang_name = lang_fields_value['instance_field'] + field_value = lang_fields_value['value'] + element_field = lang_fields_value['element_key'] + + track_changes_on_element(element, element_field, + new_value=field_value, + instance_field=field_lang_name, + original=original) + setattr(instance, field_lang_name, field_value) + + +def track_changes_on_uri_of_foreign_field(element, field_name, foreign_uri, original=None): + if original is None: + return + # get foreign uri of original + original_foreign_instance = getattr(original, field_name, '') + original_foreign_uri = '' + if original_foreign_instance: + original_foreign_uri = getattr(original_foreign_instance, 'uri', '') + track_changes_on_element(element, field_name, new_value=foreign_uri, original_value=original_foreign_uri) + + +def set_foreign_field(instance, field_name, element, original=None) -> None: if field_name not in element: return @@ -66,14 +212,23 @@ def set_foreign_field(instance, field_name, element) -> None: setattr(instance, field_name, None) return - foreign_uri = foreign_element.get('uri') + if 'uri' not in foreign_element: + message = 'Foreign model can not be assigned on {instance_model}.{field_name} {instance_uri} due to missing uri.'.format( # noqa: E501 + instance_model=instance._meta.object_name, + instance_uri=element.get('uri'), + field_name=field_name + ) + logger.info(message) + element[ImportElementFields.ERRORS].append(message) # errors is a list + track_messages_on_element(element, field_name, error=message) + return + foreign_uri = foreign_element['uri'] model_info = model_meta.get_field_info(instance) foreign_model = model_info.forward_relations[field_name].related_model - + foreign_instance = None try: foreign_instance = foreign_model.objects.get(uri=foreign_uri) - setattr(instance, field_name, foreign_instance) except foreign_model.DoesNotExist: message = '{foreign_model} {foreign_uri} for {instance_model} {instance_uri} does not exist.'.format( foreign_model=foreign_model._meta.object_name, @@ -82,57 +237,122 @@ def set_foreign_field(instance, field_name, element) -> None: instance_uri=element.get('uri') ) logger.info(message) - element['warnings'][foreign_uri].append(message) - - -def set_m2m_instances(instance, field_name, element): - if field_name not in element: - return - - foreign_elements = element.get(field_name, []) - - if not foreign_elements: - getattr(instance, field_name).clear() - return - - foreign_instances = [] - - model_info = model_meta.get_field_info(instance) - foreign_model = model_info.forward_relations[field_name].related_model + element[ImportElementFields.WARNINGS][foreign_uri].append(message) + track_messages_on_element(element, field_name, warning=message) + except foreign_model.MultipleObjectsReturned: + message = '{foreign_model} {foreign_uri} for {instance_model} {instance_uri} returns multiple objects.'.format( + foreign_model=foreign_model._meta.object_name, + foreign_uri=foreign_uri, + instance_model=instance._meta.object_name, + instance_uri=element.get('uri') + ) + logger.info(message) + element[ImportElementFields.WARNINGS][foreign_uri].append(message) + track_messages_on_element(element, field_name, warning=message) - for foreign_element in foreign_elements: - foreign_uri = foreign_element.get('uri') - try: - foreign_instance = foreign_model.objects.get(uri=foreign_uri) - foreign_instances.append(foreign_instance) - except foreign_model.DoesNotExist: - message = '{foreign_model} {foreign_uri} for {instance_model} {instance_uri} does not exist.'.format( - foreign_model=foreign_model._meta.object_name, - foreign_uri=foreign_uri, - instance_model=instance._meta.object_name, - instance_uri=element.get('uri') - ) - logger.info(message) - element['warnings'][foreign_uri].append(message) + try: + if foreign_instance is not None: + setattr(instance, field_name, foreign_instance) + _foreign_uri = foreign_uri if foreign_instance is not None else "" + track_changes_on_uri_of_foreign_field(element, + field_name, + _foreign_uri, + original=original) + except ValueError: + message = '{foreign_model} {foreign_uri} can not be assigned on {instance_model}.{field_name} {instance_uri} .'.format( # noqa: E501 + foreign_model=foreign_model._meta.object_name, + foreign_uri=foreign_uri, + instance_model=instance._meta.object_name, + instance_uri=element.get('uri'), + field_name=field_name, + ) + logger.info(message) + element[ImportElementFields.ERRORS].append(message) + track_messages_on_element(element, field_name, error=message) - getattr(instance, field_name).set(foreign_instances) +def set_extra_field(instance, field_name, element, + extra_field_helper: Optional[ExtraFieldHelper] = None, + ) -> None: -def set_m2m_through_instances(instance, field_name, element, source_name, target_name, through_name) -> None: + extra_field_value = None + if field_name in element: + extra_field_value = element.get(field_name) + else: + # get the default field value from the instance + instance_value = getattr(instance, field_name) + element[field_name] = instance_value + extra_field_value = instance_value + + if extra_field_helper is not None: + # default_value + extra_value_from_helper = extra_field_helper.get_value(instance=instance, + key=field_name) + # overwrite None or '' values by the get_value from the helper + if extra_field_value is None or extra_field_value == '': + extra_field_value = extra_value_from_helper + + if extra_field_helper.overwrite_in_element: + element[field_name] = extra_field_value + + if extra_field_value is not None: + setattr(instance, field_name, extra_field_value) + +def track_changes_m2m_instances(element, field_name, + foreign_instances, original=None): + if original is None: + return + original_m2m_instance = getattr(original, field_name) + original_m2m_instance = original_m2m_instance or [] + # m2m instance fields are unordered so comparison by set + original_uris = set(original_m2m_instance.values_list('uri', flat=True)) + foreign_uris = {i.uri for i in foreign_instances} + common_uris = list(original_uris & foreign_uris) + original_uris_list = common_uris + list(original_uris - foreign_uris) + foreign_uris_list = common_uris + list(foreign_uris - original_uris) + track_changes_on_element(element, field_name, new_value=foreign_uris_list, + original_value=original_uris_list) + + +def set_m2m_through_instances(instance, element, field_name=None, source_name=None, + target_name=None, through_name=None, + original=None, save=None) -> None: if field_name not in element: return + if not all([source_name, target_name, through_name]): + return target_elements = element.get(field_name) or [] + if isinstance(target_elements, str): + target_elements = [target_elements] model_info = model_meta.get_field_info(instance) through_model = model_info.reverse_relations[through_name].related_model target_model = model_info.forward_relations[field_name].related_model through_instances = list(getattr(instance, through_name).all()) + new_data = [] + current_data = [] + + # get the original data in correct order + if original is not None: + try: + for orig_field_instance in getattr(original, through_name).order_by(): + current_data.append({ + 'uri': getattr(orig_field_instance, target_name).uri, + 'order': orig_field_instance.order, + 'model': get_rdmo_model_path(target_name, field_name) + }) + current_data = sorted(current_data, key=lambda k: k['order']) + except AttributeError: + pass # legacy elements miss the field_name + for target_element in target_elements: target_uri = target_element.get('uri') + target_element['order'] = int(target_element['order']) # cast to int for ordering order = target_element.get('order') + new_data.append(target_element) try: target_instance = target_model.objects.get(uri=target_uri) @@ -142,20 +362,21 @@ def set_m2m_through_instances(instance, field_name, element, source_name, target through_instance = next(filter(lambda item: getattr(item, target_name).uri == target_instance.uri, through_instances)) - # update order if the item if it changed - if through_instance.order != order: + # update order of the item when it was changed + if through_instance.order != order and save: through_instance.order = order through_instance.save() - - # remove the through_instance from the through_instances list so that it won't get removed - through_instances.remove(through_instance) + if save: + # remove the through_instance from the through_instances list so that it won't get removed + through_instances.remove(through_instance) except StopIteration: # create a new item - through_model(**{ - source_name: instance, - target_name: target_instance, - 'order': order - }).save() + if save: + through_model(**{ + source_name: instance, + target_name: target_instance, + 'order': order + }).save() except target_model.DoesNotExist: message = '{target_model} {target_uri} for imported {instance_model} {instance_uri} does not exist.'.format( @@ -165,84 +386,206 @@ def set_m2m_through_instances(instance, field_name, element, source_name, target instance_uri=element.get('uri') ) logger.info(message) - element['warnings'][target_uri].append(message) + element[ImportElementFields.WARNINGS][target_uri].append(message) + element[ImportElementFields.WARNINGS][element.get('uri')].append(message) + track_messages_on_element(element, field_name, warning=message) + except target_model.MultipleObjectsReturned: + message = '{target_model} {target_uri} for imported {instance_model} {instance_uri} returns multiple objects.'.format( # noqa: E501 + target_model=target_model._meta.object_name, + target_uri=target_uri, + instance_model=instance._meta.object_name, + instance_uri=element.get('uri') + ) + logger.info(message) + element[ImportElementFields.WARNINGS][target_uri].append(message) + element[ImportElementFields.WARNINGS][element.get('uri')].append(message) + track_messages_on_element(element, field_name, warning=message) + if save: + # remove the remainders of the items list + for through_instance in through_instances: + if getattr(through_instance, target_name).uri_prefix == instance.uri_prefix: + through_instance.delete() + # sort the tracked changes by order in-place + new_data = sorted(new_data, key=lambda k: k['order']) + + track_changes_on_m2m_through_instances(element, field_name, current_data, new_data) + + +def track_changes_on_m2m_through_instances(element, field_name, current_data, new_data): + _initialize_track_changes_element_field(element, field_name) + element[ImportElementFields.DIFF][field_name][ImportElementFields.NEW] = new_data + element[ImportElementFields.DIFF][field_name][ImportElementFields.CURRENT] = current_data + new_values = [i['uri'] for i in new_data] + original_values = [i['uri'] for i in current_data] + track_changes_on_element(element, field_name, new_value=new_values, original_value=original_values) + + +def set_m2m_instances(instance, element, field_name, original=None, save=None): + if field_name not in element: + return - # remove the remainders of the items list - for through_instance in through_instances: - if getattr(through_instance, target_name).uri_prefix == instance.uri_prefix: - through_instance.delete() + foreign_elements = element.get(field_name, []) + if not foreign_elements: + getattr(instance, field_name).clear() + return -def set_reverse_m2m_through_instance(instance, field_name, element, source_name, target_name, through_name) -> None: + foreign_instances = [] + + model_info = model_meta.get_field_info(instance) + foreign_model = model_info.forward_relations[field_name].related_model + + for foreign_element in foreign_elements: + foreign_uri = foreign_element.get('uri') + + try: + foreign_instance = foreign_model.objects.get(uri=foreign_uri) + foreign_instances.append(foreign_instance) + except foreign_model.DoesNotExist: + message = '{foreign_model} {foreign_uri} for {instance_model} {instance_uri} does not exist.'.format( + foreign_model=foreign_model._meta.object_name, + foreign_uri=foreign_uri, + instance_model=instance._meta.object_name, + instance_uri=element.get('uri') + ) + logger.info(message) + element[ImportElementFields.WARNINGS][foreign_uri].append(message) + element[ImportElementFields.WARNINGS][element.get('uri')].append(message) + track_messages_on_element(element, field_name, warning=message) + if save: + getattr(instance, field_name).set(foreign_instances) + track_changes_m2m_instances(element, field_name, + foreign_instances, original=original) + + +def set_reverse_m2m_through_instance(instance, element, field_name=None, source_name=None, + target_name=None, through_name=None, + original=None, save=None) -> None: if field_name not in element: return - - target_element = element.get(field_name) + if not all([source_name, target_name, through_name]): + return + target_elements = element.get(field_name) or [] + if isinstance(target_elements, str): + target_elements = [target_elements] + elif isinstance(target_elements, dict): + target_elements = [target_elements] model_info = model_meta.get_field_info(instance) through_model = model_info.reverse_relations[through_name].related_model through_info = model_meta.get_field_info(through_model) target_model = through_info.forward_relations[target_name].related_model - target_uri = target_element.get('uri') - order = target_element.get('order') + new_data = [] + current_data = [] - try: - target_instance = target_model.objects.get(uri=target_uri) - if target_instance.is_locked: - message = '{target_model} {target_uri} for imported {instance_model} {instance_uri} is locked.'.format( + if original is not None: + try: + current_data = [] + for _through_instance in getattr(original, through_name).order_by(): + current_data.append({ + 'uri': getattr(_through_instance, source_name).uri, + 'order': _through_instance.order, + 'model': get_rdmo_model_path(target_name, field_name), + }) + current_data = sorted(current_data, key=lambda k: k['order']) + except AttributeError: + pass # legacy elements miss the field_name + + for target_element in target_elements: + target_uri = target_element.get('uri') + target_element['order'] = int(target_element['order']) # cast to int for ordering + order = target_element.get('order') + new_data.append(target_element) + + try: + target_instance = target_model.objects.get(uri=target_uri) + if target_instance.is_locked: + message = '{target_model} {target_uri} for imported {instance_model} {instance_uri} is locked.'.format( + target_model=target_model._meta.object_name, + target_uri=target_uri, + instance_model=instance._meta.object_name, + instance_uri=element.get('uri') + ) + logger.info(message) + element[ImportElementFields.ERRORS].append(message) + track_messages_on_element(element, field_name, error=message) + continue + if save: + through_instance, created = through_model.objects.get_or_create(**{ + source_name: instance, + target_name: target_instance + }) + through_instance.order = order + through_instance.save() + + except target_model.DoesNotExist: + message = '{target_model} {target_uri} for imported {instance_model} {instance_uri} does not exist.'.format( target_model=target_model._meta.object_name, target_uri=target_uri, instance_model=instance._meta.object_name, instance_uri=element.get('uri') ) logger.info(message) - element['errors'].append(message) - else: - through_instance, created = through_model.objects.get_or_create(**{ - source_name: instance, - target_name: target_instance - }) - through_instance.order = order - through_instance.save() - - except target_model.DoesNotExist: - message = '{target_model} {target_uri} for imported {instance_model} {instance_uri} does not exist.'.format( - target_model=target_model._meta.object_name, - target_uri=target_uri, - instance_model=instance._meta.object_name, - instance_uri=element.get('uri') - ) - logger.info(message) - element['warnings'][target_uri].append(message) + element[ImportElementFields.WARNINGS][target_uri].append(message) + element[ImportElementFields.WARNINGS][element.get('uri')].append(message) + track_messages_on_element(element, field_name, warning=message) + # sort the tracked changes by order in-place + new_data = sorted(new_data, key=lambda k: k['order']) + + track_changes_on_m2m_through_instances(element, field_name, current_data, new_data) + + +def format_message_from_validation_error(exception: ValidationError) -> str: + message = '; '.join(['{}: {}'.format(key, ', '.join(messages)) for key, messages in exception.message_dict.items()]) + return message def validate_instance(instance, element, *validators): exception_message = None try: instance.full_clean() - for validator in validators: - validator(instance if instance.id else None)(vars(instance)) except ValidationError as e: - try: - exception_message = '; '.join(['{}: {}'.format(key, ', '.join(messages)) - for key, messages in e.message_dict.items()]) - except AttributeError: - exception_message = ''.join(e.messages) - except ObjectDoesNotExist as e: - exception_message = e - finally: - if exception_message is not None: - message = '{instance_model} {instance_uri} cannot be imported ({exception}).'.format( - instance_model=instance._meta.object_name, - instance_uri=element.get('uri'), - exception=exception_message - ) - logger.info(message) - element['errors'].append(message) - + exception_message = format_message_from_validation_error(e) + message = '{instance_model} {instance_uri} cannot be imported ({exception}).'.format( + instance_model=instance._meta.object_name, + instance_uri=element.get('uri'), + exception=exception_message + ) + logger.info(message) + _key = "FullClean" + element[ImportElementFields.ERRORS].append(message) + track_messages_on_element(element, _key, error=message) + return -def check_permissions(instance, element, user): + for validator in validators: + if issubclass(validator, LockedValidator): + element['locked'] = False + try: + validator(instance=instance if instance.id else None)(vars(instance)) + except ValidationError as e: + try: + exception_message = format_message_from_validation_error(e) + except AttributeError: + exception_message = ''.join(e.messages) + except ObjectDoesNotExist as e: + exception_message = e + finally: + if exception_message is not None: + message = '{instance_model} {instance_uri} cannot be imported ({exception}).'.format( + instance_model=instance._meta.object_name, + instance_uri=element.get('uri'), + exception=exception_message + ) + logger.info(message) + _key = validator.__qualname__ + element[ImportElementFields.ERRORS].append(message) + track_messages_on_element(element, _key, error=message) + if issubclass(validator, LockedValidator): + element['locked'] = True + + +def check_permissions(instance: models.Model, element_uri: str, user: models.Model) -> Optional[str]: if user is None: return @@ -255,9 +598,6 @@ def check_permissions(instance, element, user): perms = [f'{app_label}.add_{model_name}_object'] if not user.has_perms(perms, instance): - message = 'You have no permissions to import {instance_model} {instance_uri}.'.format( - instance_model=instance._meta.object_name, - instance_uri=element.get('uri') - ) + message = _('You have no permissions to import') + f' {instance._meta.object_name} {element_uri}.' logger.info(message) - element['errors'].append(message) + return message diff --git a/rdmo/core/management/commands/download_vendor_files.py b/rdmo/core/management/commands/download_vendor_files.py index 2c3e8935f8..dc51b48b2b 100644 --- a/rdmo/core/management/commands/download_vendor_files.py +++ b/rdmo/core/management/commands/download_vendor_files.py @@ -52,4 +52,4 @@ def handle(self, *args, **options): h = hashlib.new(algorithm) h.update(open(file_name, 'rb').read()) if base64.b64encode(h.digest()).decode() != file_hash: - raise Exception('Subresource Integrity (SRI) failed for %s' % file_name) + raise Exception(f'Subresource Integrity (SRI) failed for {file_name}') diff --git a/rdmo/core/management/commands/setup_groups.py b/rdmo/core/management/commands/setup_groups.py index 434fcb8899..fb3467aebb 100644 --- a/rdmo/core/management/commands/setup_groups.py +++ b/rdmo/core/management/commands/setup_groups.py @@ -13,7 +13,7 @@ def handle(self, *args, **options): group, created = Group.objects.get_or_create(name=name) if created: - print('Group "%s" created' % name) + print(f'Group "{name}" created') else: group.permissions.clear() diff --git a/rdmo/core/permissions.py b/rdmo/core/permissions.py index 6cdc1b5662..cd6402de35 100644 --- a/rdmo/core/permissions.py +++ b/rdmo/core/permissions.py @@ -74,3 +74,14 @@ def has_permission(self, request, view): return True else: return False + + +class CanToggleElementCurrentSite(DjangoModelPermissions): + + perms_map = { + 'PUT': ['%(app_label)s.change_%(model_name)s_toggle_site'], + } + + @log_result + def has_permission(self, request, view): + return super().has_permission(request, view) diff --git a/rdmo/core/renderers.py b/rdmo/core/renderers.py index 987cff907f..91e7e8417b 100644 --- a/rdmo/core/renderers.py +++ b/rdmo/core/renderers.py @@ -1,3 +1,4 @@ +import re from io import StringIO from django.utils.encoding import smart_str @@ -37,7 +38,10 @@ def render_text_element(self, xml, tag, attrs, text): xml.startElement(tag, attrs) if text is not None: - xml.characters(smart_str(text)) + smart_text = smart_str(text) + # remove control chars, cp. https://github.com/django/django/blob/main/django/utils/xmlutils.py#L25 + smart_text = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F]', '', smart_text) + xml.characters(smart_text) xml.endElement(tag) def render_document(self, xml, data): diff --git a/rdmo/core/serializers.py b/rdmo/core/serializers.py index 1c06cc8a50..95abe722a5 100644 --- a/rdmo/core/serializers.py +++ b/rdmo/core/serializers.py @@ -38,7 +38,7 @@ def to_representation(self, instance): response = super().to_representation(instance) for markdown_field in self.markdown_fields: - if markdown_field in response and response[markdown_field]: + if response.get(markdown_field): response[markdown_field] = markdown2html(response[markdown_field]) return response @@ -216,7 +216,10 @@ def construct_object_permission(model, action_name: str) -> str: return perm def get_read_only(self, obj) -> bool: - user = self.context['request'].user + request = self.context.get('request') + if request is None: + return False + user = request.user perms = (self.construct_object_permission(self.Meta.model, action_name) for action_name in self.OBJECT_PERMISSION_ACTION_NAMES) return not all(user.has_perm(perm, obj) for perm in perms) diff --git a/rdmo/core/settings.py b/rdmo/core/settings.py index dcd176b4e5..e7cb03ba58 100644 --- a/rdmo/core/settings.py +++ b/rdmo/core/settings.py @@ -1,11 +1,11 @@ +import re + from django.utils.translation import gettext_lazy as _ SITE_ID = 1 DEBUG = False -ALLOWED_HOSTS = ['localhost', 'ip6-localhost', '127.0.0.1', '[::1]'] - INSTALLED_APPS = [ # django modules 'django.contrib.admin', @@ -101,7 +101,11 @@ ACCOUNT_GROUPS = [] ACCOUNT_TERMS_OF_USE = False ACCOUNT_ADAPTER = 'rdmo.accounts.adapter.AccountAdapter' -ACCOUNT_SIGNUP_FORM_CLASS = 'rdmo.accounts.forms.SignupForm' +ACCOUNT_FORMS = { + 'login': 'rdmo.accounts.forms.LoginForm', + 'signup': 'rdmo.accounts.forms.SignupForm' + +} ACCOUNT_USER_DISPLAY = 'rdmo.accounts.utils.get_full_name' ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_ACTIVATION_DAYS = 7 @@ -119,6 +123,7 @@ SOCIALACCOUNT_GROUPS = [] SOCIALACCOUNT_AUTO_SIGNUP = False SOCIALACCOUNT_ADAPTER = 'rdmo.accounts.adapter.SocialAccountAdapter' +SOCIALACCOUNT_OPENID_CONNECT_URL_PREFIX = "" # required since 0.60.0 else default is "oidc" SHIBBOLETH = False SHIBBOLETH_LOGIN_URL = '/Shibboleth.sso/Login' @@ -214,6 +219,7 @@ 'MULTISITE', 'GROUPS', 'EXPORT_FORMATS', + 'PROJECT_TABLE_PAGE_SIZE' ] EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' @@ -272,6 +278,8 @@ EXPORT_CONTENT_DISPOSITION = 'attachment' +PROJECT_TABLE_PAGE_SIZE = 20 + PROJECT_ISSUES = True PROJECT_ISSUE_PROVIDERS = [] @@ -314,6 +322,56 @@ OPTIONSET_PROVIDERS = [] +PROJECT_VALUES_VALIDATION = False + +PROJECT_VALUES_VALIDATION_URL = True + +PROJECT_VALUES_VALIDATION_INTEGER = True +PROJECT_VALUES_VALIDATION_INTEGER_MESSAGE = _('Enter a valid integer.') +PROJECT_VALUES_VALIDATION_INTEGER_REGEX = re.compile(r'^[+-]?\d+$') + +PROJECT_VALUES_VALIDATION_FLOAT = True +PROJECT_VALUES_VALIDATION_FLOAT_MESSAGE = _('Enter a valid float.') +PROJECT_VALUES_VALIDATION_FLOAT_REGEX = re.compile(r''' + ^[+-]? # Optional sign + ( + \d+ # Digits before the decimal or thousands separator + (,\d{3})* # Optional groups of exactly three digits preceded by a comma (thousands separator) + (\.\d+)? # Optional decimal part, a dot followed by one or more digits + | # OR + \d+ # Digits before the decimal or thousands separator + (\.\d{3})* # Optional groups of exactly three digits preceded by a dot (thousands separator) + (,\d+)? # Optional decimal part, a comma followed by one or more digits + ) + ([eE][+-]?\d+)?$ # Optional exponent part +''', re.VERBOSE) + +PROJECT_VALUES_VALIDATION_BOOLEAN = True +PROJECT_VALUES_VALIDATION_BOOLEAN_MESSAGE = _('Enter a valid boolean (e.g. 0, 1).') +PROJECT_VALUES_VALIDATION_BOOLEAN_REGEX = r'(?i)^(0|1|f|t|false|true)$' + +PROJECT_VALUES_VALIDATION_DATE = True +PROJECT_VALUES_VALIDATION_DATE_MESSAGE = _('Enter a valid date (e.g. "02.03.2024", "03/02/2024", "2024-02-03").') +PROJECT_VALUES_VALIDATION_DATE_REGEX = re.compile(r''' + ^( + \d{1,2}\.\s*\d{1,2}\.\s*\d{2,4} # Format dd.mm.yyyy + | \d{1,2}/\d{1,2}/\d{4} # Format mm/dd/yyyy + | \d{4}-\d{2}-\d{2} # Format yyyy-mm-dd + )$ +''', re.VERBOSE) + +PROJECT_VALUES_VALIDATION_DATETIME = True + +PROJECT_VALUES_VALIDATION_EMAIL = True + +PROJECT_VALUES_VALIDATION_PHONE = True +PROJECT_VALUES_VALIDATION_PHONE_MESSAGE = _('Enter a valid phone number (e.g. "123456" or "+49 (0) 30 123456").') +PROJECT_VALUES_VALIDATION_PHONE_REGEX = re.compile(r''' + ^([+]\d{2,3}\s)? # Optional country code + (\(\d+\)\s)? # Optional area code in parentheses + [\d\s]*$ # Main number with spaces +''', re.VERBOSE) + QUESTIONS_WIDGETS = [ ('text', _('Text'), 'rdmo.projects.widgets.TextWidget'), ('textarea', _('Textarea'), 'rdmo.projects.widgets.TextareaWidget'), diff --git a/rdmo/core/static/core/css/base.scss b/rdmo/core/static/core/css/base.scss index 44bf835add..ecf2056c51 100644 --- a/rdmo/core/static/core/css/base.scss +++ b/rdmo/core/static/core/css/base.scss @@ -35,6 +35,10 @@ h4 { form { margin-bottom: 20px; } +textarea { + resize: vertical; +} + .extend { width: 100%; } @@ -79,6 +83,24 @@ a { &.disabled { cursor: not-allowed; } + + &.fa { + text-decoration: none !important; + + &:visited, + &:hover, + &:focus { + text-decoration: none !important; + } + } +} + +.btn-link { + color: $link-color; + + &:hover { + color: $link-color-hover; + } } code { @@ -192,8 +214,7 @@ metadata { .page h2:nth-child(2) { margin-top: 0; } -.sidebar h2:first-child, -.sidebar .import-buttons { +.sidebar h2:first-child { margin-top: 70px; } diff --git a/rdmo/core/static/core/css/fonts.scss b/rdmo/core/static/core/css/fonts.scss index 2b5dcecc40..233e08076f 100644 --- a/rdmo/core/static/core/css/fonts.scss +++ b/rdmo/core/static/core/css/fonts.scss @@ -39,6 +39,8 @@ h1, h2, h3, h4, h5, h6 { font-family: DroidSerif, serif; } -a.fa { - text-decoration: none !important; +.react-datepicker { + h1, h2, h3, h4, h5, h6 { + font-family: DroidSans, sans; + } } diff --git a/rdmo/core/templates/core/about_text_de.html b/rdmo/core/templates/core/about_text_de.html index f0d2a71023..a6dfaae7a5 100644 --- a/rdmo/core/templates/core/about_text_de.html +++ b/rdmo/core/templates/core/about_text_de.html @@ -13,10 +13,7 @@

Version {% version %}

Mit dem Research Data Management Organiser (RDMO) können Institutionen und Forschende das Forschungsdatenmanagement ihre Projekte strukturiert planen und durchführen. Es erlaubt das Erfassen aller relevanten Planungsinformationen in Datenmanagementplänen und die Verwaltung aller Datenmanagementaufgaben über den gesamten Datenlebenszyklus. -

- -

- RDMO ist bereit für die Anwendung in kleineren und größeren Projekten. In der zweiten Phase des RDMO-Projekts seit November 2017 erweitern die Projektpartner AIP, FHP und KIT-Bibliothek die bereits veröffentlichte Version des RDMO und arbeiten vertieft mit Anwendern zusammen. Im Fokus der Erweiterung von RDMO stehen u.a. Rollenkonzepte und Anbindungen an die institutionelle Infrastruktur wie Repositorien, Ticket-Systeme und Infrastruktur für Authentifizierung und Autorisierung. Es werden Workshops zur Diskussion der Funktionalitäten durchgeführt, Einführungsmaterialien erstellt und eine Verbreiterung der Entwickler-Community angestrebt. + RDMO wurde im Rahmen einer Kooperation zwischen dem Leibniz-Instituts für Astrophysik Potsdam, der Fachhochschule Potsdam und der Bibliothek des Karlsruher Instituts für Technologie entwickelt und wird heute durch die RDMO Arbeitsgemeinschaft gepflegt, weiterentwickelt und verstetigt.

diff --git a/rdmo/core/templates/core/about_text_en.html b/rdmo/core/templates/core/about_text_en.html index 657676c360..ff252e8139 100644 --- a/rdmo/core/templates/core/about_text_en.html +++ b/rdmo/core/templates/core/about_text_en.html @@ -12,11 +12,8 @@

Version {% version %}

- The Research Data Management Organiser (RDMO) enables institutions as well as researchers to plan and carry out their management of research data. RDMO can assemble all relevant planning information and data management tasks across the whole life cycle of the research data. -

- -

- RDMO is ready for application in smaller or bigger projects. In the next projectphase, which started November 2017, the RDMO tool will be extended and the project partners AIP, FHP, and KIT Library will collaborate with the RDMO users to improve its usage. The tool will be extended by enhancing its implementation of roles and interfaces to institutional infrastructure, e.g. repositories, ticketing systems, and the infrastructure for authentication and authorization. Tutorials, documentation and other material are planned for dissemination, and workshops for users and developers. + The Research Data Management Organiser (RDMO) enables institutions as well as researchers to plan and carry out their management of research data in a structured manner. RDMO can assemble all relevant planning information and data management tasks across the whole life cycle of the research data. + RDMO was developed within a cooperation between the Leibniz Institute for Astrophysics Potsdam, the University of Applied Sciences of Potsdam and the library of the Karlsruhe Institute of Technology and is now maintained, further developed and consolidated by the RDMO working group.

diff --git a/rdmo/core/templates/core/about_text_es.html b/rdmo/core/templates/core/about_text_es.html new file mode 100644 index 0000000000..063c9ec1a0 --- /dev/null +++ b/rdmo/core/templates/core/about_text_es.html @@ -0,0 +1,22 @@ +{% load static %} +{% load core_tags %} +{% load i18n %} + +

+ + +

Research data management organiser

+ +

Version {% version %}

+
+ +
+

+ Con el Organizador de Gestión de Datos de Investigación (RDMO), las instituciones y los investigadores pueden planificar y llevar a cabo la gestión de datos de investigación de sus proyectos de forma estructurada. RDMO permite la integración de toda la información de planificación relevante en los planes de gestión de datos y la administración de las tareas de gestión de datos a lo largo de todo el ciclo de vida de los datos. + RDMO se desarrolló en el marco de una cooperación entre el Instituto Leibniz de Astrofísica de Potsdam, la Universidad de Ciencias Aplicadas de Potsdam y la biblioteca del Instituto de Tecnología de Karlsruhe, y en la actualidad es mantenido, desarrollado y consolidado por el grupo de trabajo RDMO. +

+ +

+ Encontrará más información sobre RDMO en rdmorganiser.github.io. +

+
diff --git a/rdmo/core/templates/core/about_text_fr.html b/rdmo/core/templates/core/about_text_fr.html index 8f38ec2b69..d88db71f96 100644 --- a/rdmo/core/templates/core/about_text_fr.html +++ b/rdmo/core/templates/core/about_text_fr.html @@ -12,11 +12,8 @@

Version {% version %}

- RDMO permet aux institutions ainsi qu'aux chercheurs de planifier et de réaliser leur gestion des données de recherche. RDMO peut rassembler toutes les informations de planification pertinentes et les tâches de gestion des données tout au long du cycle de vie des données de recherche. -

- -

- RDMO est prêt à être appliqué dans des projets plus ou moins importants. Dans la prochaine phase du projet, qui a débuté en novembre 2017, l'outil RDMO sera étendu et les partenaires du projet, l'AIP, la FHP et la bibliothèque du KIT, collaboreront avec les utilisateurs de RDMO pour améliorer son utilisation. L'outil sera étendu en améliorant sa mise en œuvre des rôles et des interfaces avec l'infrastructure institutionnelle, par exemple les dépôts, les systèmes de billetterie, et l'infrastructure d'authentification et d'autorisation. Des tutoriels, de la documentation et d'autres matériels sont prévus pour la diffusion, ainsi que des ateliers pour les utilisateurs et les développeurs. + Le Research Data Management Organiser (RDMO) permet aux institutions et aux chercheurs de planifier et de réaliser la gestion des données de recherche de leurs projets de manière structurée. Il permet de saisir toutes les informations de planification pertinentes dans des plans de gestion des données et de gérer toutes les tâches de gestion des données tout au long du cycle de vie des données. + RDMO a été développé dans le cadre d'une coopération entre le Leibniz-Institut für Astrophysik Potsdam, la Fachhochschule Potsdam et la bibliothèque du Karlsruher Institut für Technologie et est aujourd'hui entretenu, développé et pérennisé par le groupe de travail RDMO.

diff --git a/rdmo/core/templates/core/about_text_it.html b/rdmo/core/templates/core/about_text_it.html index 784c25ec38..8830f41606 100644 --- a/rdmo/core/templates/core/about_text_it.html +++ b/rdmo/core/templates/core/about_text_it.html @@ -12,15 +12,8 @@

Versione {% version %}

- RDMO consente alle istituzioni e ai singoli ricercatori la pianificazione della gestione dei dati della ricerca. RDMO assembla tutte le informazioni riguardanti la pianificazione e la gestione - dei dati durante l'intera vita dei dati della ricerca. -

- -

- RDMO può essere usata in progetti grossi e piccoli. Nella prossima fase del progetto, iniziata nel Novembre 2017, RDMO sarà esteso e i partner del progetto - AIP, FHP, biblioteca KIT collaboreranno con gli utenti RDMO per migliorarne l'utilizzo. Questo strumento sarà esteso, migliorando l'implementazione dei ruoli e dell'interfacciamento - con le infrastrutture istituzionali, ad es. repositories, sistemi di ticketing, e infrastruttura di autenticazione e autorizzazione. Verranno anche scritti tutorial e documentazione, e - organizzati workshop per utenti e sviluppatori. + Con Research Data Management Organiser (RDMO), istituzioni e ricercatori possono pianificare e implementare la gestione dei dati di ricerca dei loro progetti in modo strutturato. Consente di registrare tutte le informazioni rilevanti per la pianificazione nei piani di gestione dei dati e di gestire tutte le attività di gestione dei dati per l'intero ciclo di vita dei dati. + RDMO è stato sviluppato nell'ambito di una collaborazione tra l'Istituto Leibniz per l'Astrofisica di Potsdam, l'Università di Scienze Applicate di Potsdam e la biblioteca dell'Istituto di Tecnologia di Karlsruhe ed è ora mantenuto, ulteriormente sviluppato e consolidato dal gruppo di lavoro RDMO.

diff --git a/rdmo/core/templates/core/base_head.html b/rdmo/core/templates/core/base_head.html index 23f48f7074..d5e28ea95e 100644 --- a/rdmo/core/templates/core/base_head.html +++ b/rdmo/core/templates/core/base_head.html @@ -1,4 +1,7 @@ {% load static %} +{% load i18n %} +{% get_current_language as LANGUAGE_CODE %} + {{ request.site.name }} @@ -10,5 +13,6 @@ + diff --git a/rdmo/core/templates/core/footer_text_es.html b/rdmo/core/templates/core/footer_text_es.html new file mode 100644 index 0000000000..0a232249aa --- /dev/null +++ b/rdmo/core/templates/core/footer_text_es.html @@ -0,0 +1,23 @@ +

+ + + +
diff --git a/rdmo/core/templates/core/home.html b/rdmo/core/templates/core/home.html index bb95e20caa..dcf6e160e9 100644 --- a/rdmo/core/templates/core/home.html +++ b/rdmo/core/templates/core/home.html @@ -29,7 +29,7 @@
diff --git a/rdmo/core/templates/core/home_text_de.html b/rdmo/core/templates/core/home_text_de.html index cfba3c8a9c..ff500446e4 100644 --- a/rdmo/core/templates/core/home_text_de.html +++ b/rdmo/core/templates/core/home_text_de.html @@ -1,13 +1,11 @@

Willkommen bei RDMO

- Das Ziel des RDMO Projekts ist es, eine Webapplikation bereitzustellen, die die strukturierte Planung, Umsetzung und Verwaltung der Daten in einem wissenschaftlichen Projekt unterstützt. Zusätzlich sollen die gesammelten Informationen in textueller Form für Anforderungen von Förderern oder für Berichte ausgebbar sein. + RDMO ist eine Webapplikation, die die strukturierte Planung, Umsetzung und Verwaltung der Daten in einem wissenschaftlichen Projekt unterstützt.
+ Die gesammelten Informationen können in Forschungsworkflows weiterverwendet werden und zusätzlich in textueller Form ausgegeben werden - + als Datenmanagementplan für die Förderer oder als teil eines Forschungsberichts.

- Diese Webseite ist ein Prototyp zur Veranschaulichung der bereits implementierten Funktionen. -

- -

- Wenn Sie mehr über das Projekt erfahren wollen, besuchen Sie rdmorganiser.github.io. + Wenn Sie mehr über die Nutzung von RDMO als Endnutzer oder Manager erfahren wollen, besuchen Sie rdmorganiser.github.io.

diff --git a/rdmo/core/templates/core/home_text_en.html b/rdmo/core/templates/core/home_text_en.html index 1eb966979a..de9dcebfc2 100644 --- a/rdmo/core/templates/core/home_text_en.html +++ b/rdmo/core/templates/core/home_text_en.html @@ -1,11 +1,8 @@

Welcome to RDMO

- The aim of the RDMO project is to deliver a web application to assist structured planning, implementation and administration of the data in a scientific project. Additionally, the gathered information can be cast into textual forms suitable for funding agencies requirements or for reports. -

- -

- This is a prototype of the software, for demonstration purposes. + RDMO is a web application that supports the structured planning, realisation and management of data in a scientific project.
+ The information collected can be used in research workflows and can also be output in textual form - as a data management plan for funders or as part of a research report.

diff --git a/rdmo/core/templates/core/home_text_es.html b/rdmo/core/templates/core/home_text_es.html new file mode 100644 index 0000000000..fce115f690 --- /dev/null +++ b/rdmo/core/templates/core/home_text_es.html @@ -0,0 +1,10 @@ +

Bienvenido a RDMO

+ +

+ RDMO es una aplicación web que apoya la planificación estructurada, la realización y la gestión de datos en un proyecto científico.
+ La información recopilada puede utilizarse en flujos de trabajo de investigación y también puede ser exportada en forma de texto: como plan de gestión de datos para los financiadores o como parte de un informe de investigación. +

+ +

+ Si desea obtener más información sobre el uso de RDMO como usuario final o gestor, visite rdmorganiser.github.io. +

diff --git a/rdmo/core/templates/core/home_text_fr.html b/rdmo/core/templates/core/home_text_fr.html index 7b806ad812..e4ace4f14b 100644 --- a/rdmo/core/templates/core/home_text_fr.html +++ b/rdmo/core/templates/core/home_text_fr.html @@ -1,11 +1,9 @@

Bienvenue sur RDMO

- L'objectif du projet RDMO est de fournir une application web pour aider à la planification structurée, à la mise en œuvre et à l'administration des données d'un projet scientifique. En outre, les informations recueillies peuvent être transposées sous forme textuelle pour répondre aux exigences des organismes de financement ou pour les rapports. -

- -

- Il s'agit d'un prototype du logiciel, à des fins de démonstration. + RDMO est une application web qui soutient la planification structurée, la mise en œuvre et la gestion des données dans un projet scientifique.
+ Les informations collectées peuvent être réutilisées dans des workflows de recherche et peuvent en outre être éditées sous forme de texte - + comme plan de gestion des données pour les bailleurs de fonds ou comme partie d'un rapport de recherche.

diff --git a/rdmo/core/templates/core/home_text_it.html b/rdmo/core/templates/core/home_text_it.html index bf684776ff..b0ca5c4409 100644 --- a/rdmo/core/templates/core/home_text_it.html +++ b/rdmo/core/templates/core/home_text_it.html @@ -1,11 +1,8 @@

Benvenuti a RDMO

- L'obiettivo del progetto RDMO è fornire un'applicazione web per aiutare la pianificazione, l'implementazione e l'amministrazione dei dati in un progetto scientifico. In aggiunta, le informazioni raccolte possono essere stampate in formato testuale per essere inviate ai finanziatori. -

- -

- Questo è un prototipo del software, per soli scopi dimostrativi. + RDMO è un'applicazione web che supporta la pianificazione, la realizzazione e la gestione strutturata dei dati in un progetto scientifico.
+ Le informazioni raccolte possono essere utilizzate nei flussi di lavoro della ricerca e possono anche essere prodotte in forma testuale - come piano di gestione dei dati per i finanziatori o come parte di una relazione di ricerca.

diff --git a/rdmo/core/templates/core/projects_page.html b/rdmo/core/templates/core/projects_page.html new file mode 100644 index 0000000000..be3cc66d7f --- /dev/null +++ b/rdmo/core/templates/core/projects_page.html @@ -0,0 +1,13 @@ +{% extends 'core/base.html' %} + +{% block content %} + +

+
+
+ {% block page %}{% endblock %} +
+
+
+ +{% endblock %} diff --git a/rdmo/core/tests/__init__.py b/rdmo/core/tests/__init__.py index 943cba0608..e69de29bb2 100644 --- a/rdmo/core/tests/__init__.py +++ b/rdmo/core/tests/__init__.py @@ -1,148 +0,0 @@ - -from rdmo.core.models import Model - -multisite_users = ( - ('user', 'user'), - ('reviewer', 'reviewer'), - ('editor', 'editor'), - ('example-reviewer', 'example-reviewer'), - ('example-editor', 'example-editor'), - ('foo-user', 'foo-user'), - ('foo-reviewer', 'foo-reviewer'), - ('foo-editor', 'foo-editor'), - ('bar-user', 'bar-user'), - ('bar-reviewer', 'bar-reviewer'), - ('bar-editor', 'bar-editor'), - ('anonymous', None), -) - - -multisite_status_map = { - 'list': { - 'foo-user': 403, 'foo-reviewer': 200, 'foo-editor': 200, - 'bar-user': 403, 'bar-reviewer': 200, 'bar-editor': 200, - 'user': 403, 'example-reviewer': 200, 'example-editor': 200, - 'anonymous': 401, 'reviewer': 200, 'editor': 200, - }, - 'detail': { - 'foo-user': 404, 'foo-reviewer': 200, 'foo-editor': 200, - 'bar-user': 404, 'bar-reviewer': 200, 'bar-editor': 200, - 'user': 404, 'example-reviewer': 200, 'example-editor': 200, - 'anonymous': 401, 'reviewer': 200, 'editor': 200, - }, - 'nested': { - 'foo-user': 404, 'foo-reviewer': 200, 'foo-editor': 200, - 'bar-user': 404, 'bar-reviewer': 200, 'bar-editor': 200, - 'user': 404, 'example-reviewer': 200, 'example-editor': 200, - 'anonymous': 401, 'reviewer': 200, 'editor': 200, - }, - 'create': { - 'foo-user': 403, 'foo-reviewer': 403, 'foo-editor': 201, - 'bar-user': 403, 'bar-reviewer': 403, 'bar-editor': 201, - 'user': 403, 'example-reviewer': 403, 'example-editor': 201, - 'anonymous': 401, 'reviewer': 403, 'editor': 201, - }, - 'copy': { - 'foo-user': 404, 'foo-reviewer': 403, 'foo-editor': 201, - 'bar-user': 404, 'bar-reviewer': 403, 'bar-editor': 201, - 'user': 404, 'example-reviewer': 403, 'example-editor': 201, - 'anonymous': 401, 'reviewer': 403, 'editor': 201, - }, - 'update': { - 'foo-user': 404, 'foo-reviewer': 403, 'foo-editor': 200, - 'bar-user': 404, 'bar-reviewer': 403, 'bar-editor': 200, - 'user': 404, 'example-reviewer': 403, 'example-editor': 200, - 'anonymous': 401, 'reviewer': 403, 'editor': 200, - }, - 'delete': { - 'foo-user': 404, 'foo-reviewer': 403, 'foo-editor': 204, - 'bar-user': 404, 'bar-reviewer': 403, 'bar-editor': 204, - 'user': 404, 'example-reviewer': 403, 'example-editor': 204, - 'anonymous': 401, 'reviewer': 403, 'editor': 204, - } -} - - -status_map_object_permissions = { - 'copy': { - 'all-element': { - 'foo-reviewer': 403, 'foo-editor': 201, - 'bar-reviewer': 403, 'bar-editor': 201, - 'example-reviewer': 403, 'example-editor': 201, - }, - 'foo-element': { - 'foo-reviewer': 403, 'foo-editor': 201, - 'bar-reviewer': 404, 'bar-editor': 404, - 'example-reviewer': 404, 'example-editor': 404, - }, - 'bar-element': { - 'foo-reviewer': 404, 'foo-editor': 404, - 'bar-reviewer': 403, 'bar-editor': 201, - 'example-reviewer': 404, 'example-editor': 404, - } - }, - 'update': { - 'all-element': { - 'foo-reviewer': 403, 'foo-editor': 200, - 'bar-reviewer': 403, 'bar-editor': 200, - 'example-reviewer': 403, 'example-editor': 200, - }, - 'foo-element': { - 'foo-reviewer': 403, 'foo-editor': 200, - 'bar-reviewer': 404, 'bar-editor': 404, - 'example-reviewer': 404, 'example-editor': 404, - }, - 'bar-element': { - 'foo-reviewer': 404, 'foo-editor': 404, - 'bar-reviewer': 403, 'bar-editor': 200, - 'example-reviewer': 404, 'example-editor': 404, - } - }, - 'delete': { - 'all-element': { - 'foo-reviewer': 403, 'foo-editor': 204, - 'bar-reviewer': 403, 'bar-editor': 204, - 'example-reviewer': 403, 'example-editor': 204, - }, - 'foo-element': { - 'foo-reviewer': 403, 'foo-editor': 204, - 'bar-reviewer': 404, 'bar-editor': 404, - 'example-reviewer': 404, 'example-editor': 404, - }, - 'bar-element': { - 'foo-reviewer': 404, 'foo-editor': 404, - 'bar-reviewer': 403, 'bar-editor': 204, - 'example-reviewer': 404, 'example-editor': 404, - } - }, -} - - -def get_obj_perms_status_code(instance, username, method): - ''' looks for the object permissions of the instance and returns the status code ''' - - if isinstance(instance, Model): - try: - if not instance.editors.exists(): - return multisite_status_map[method][username] - except AttributeError as e: - raise AttributeError(f'instance {instance} should have an editors attribute') from e - elif isinstance(instance, str): - pass - - if 'foo-' in str(instance): - instance_obj_perms_key = 'foo-element' - elif 'bar-' in str(instance): - instance_obj_perms_key = 'bar-element' - else: - instance_obj_perms_key = 'all-element' - - try: - method_instance_obj_perms_map = status_map_object_permissions[method][instance_obj_perms_key] - except KeyError as e: - raise KeyError(f'instance ({instance_obj_perms_key}) should be defined in status_map_object_permissions') from e - try: - return method_instance_obj_perms_map[username] - except KeyError: - # not all users are defined in the method_instance_perms_map - return multisite_status_map[method][username] diff --git a/rdmo/core/tests/constants.py b/rdmo/core/tests/constants.py new file mode 100644 index 0000000000..f8e26a30e8 --- /dev/null +++ b/rdmo/core/tests/constants.py @@ -0,0 +1,139 @@ + +multisite_status_map = { + 'list': { + 'foo-user': 403, 'foo-reviewer': 200, 'foo-editor': 200, + 'bar-user': 403, 'bar-reviewer': 200, 'bar-editor': 200, + 'user': 403, 'example-reviewer': 200, 'example-editor': 200, + 'anonymous': 401, 'reviewer': 200, 'editor': 200, + }, + 'detail': { + 'foo-user': 404, 'foo-reviewer': 200, 'foo-editor': 200, + 'bar-user': 404, 'bar-reviewer': 200, 'bar-editor': 200, + 'user': 404, 'example-reviewer': 200, 'example-editor': 200, + 'anonymous': 401, 'reviewer': 200, 'editor': 200, + }, + 'nested': { + 'foo-user': 404, 'foo-reviewer': 200, 'foo-editor': 200, + 'bar-user': 404, 'bar-reviewer': 200, 'bar-editor': 200, + 'user': 404, 'example-reviewer': 200, 'example-editor': 200, + 'anonymous': 401, 'reviewer': 200, 'editor': 200, + }, + 'create': { + 'foo-user': 403, 'foo-reviewer': 403, 'foo-editor': 201, + 'bar-user': 403, 'bar-reviewer': 403, 'bar-editor': 201, + 'user': 403, 'example-reviewer': 403, 'example-editor': 201, + 'anonymous': 401, 'reviewer': 403, 'editor': 201, + }, + 'copy': { + 'foo-user': 404, 'foo-reviewer': 403, 'foo-editor': 201, + 'bar-user': 404, 'bar-reviewer': 403, 'bar-editor': 201, + 'user': 404, 'example-reviewer': 403, 'example-editor': 201, + 'anonymous': 401, 'reviewer': 403, 'editor': 201, + }, + 'update': { + 'foo-user': 404, 'foo-reviewer': 403, 'foo-editor': 200, + 'bar-user': 404, 'bar-reviewer': 403, 'bar-editor': 200, + 'user': 404, 'example-reviewer': 403, 'example-editor': 200, + 'anonymous': 401, 'reviewer': 403, 'editor': 200, + }, + 'delete': { + 'foo-user': 404, 'foo-reviewer': 403, 'foo-editor': 204, + 'bar-user': 404, 'bar-reviewer': 403, 'bar-editor': 204, + 'user': 404, 'example-reviewer': 403, 'example-editor': 204, + 'anonymous': 401, 'reviewer': 403, 'editor': 204, + }, + 'toggle-site': { + # foo-editor is not permitted to apply own site(foo.com) in test run(example.com) + 'foo-user': 403, 'foo-reviewer': 403, 'foo-editor': 403, + # bar-editor is not permitted to apply own site(bar.com) in test run(example.com) + 'bar-user': 403, 'bar-reviewer': 403, 'bar-editor': 403, + 'user': 403, 'example-reviewer': 403, 'example-editor': 200, + 'anonymous': 401, 'reviewer': 403, 'editor': 200, + }, +} +status_map_object_permissions = { + 'copy': { + 'all-element': { + 'foo-reviewer': 403, 'foo-editor': 201, + 'bar-reviewer': 403, 'bar-editor': 201, + 'example-reviewer': 403, 'example-editor': 201, + }, + 'foo-element': { + 'foo-reviewer': 403, 'foo-editor': 201, + 'bar-reviewer': 404, 'bar-editor': 404, + 'example-reviewer': 404, 'example-editor': 404, + }, + 'bar-element': { + 'foo-reviewer': 404, 'foo-editor': 404, + 'bar-reviewer': 403, 'bar-editor': 201, + 'example-reviewer': 404, 'example-editor': 404, + } + }, + 'update': { + 'all-element': { + 'foo-reviewer': 403, 'foo-editor': 200, + 'bar-reviewer': 403, 'bar-editor': 200, + 'example-reviewer': 403, 'example-editor': 200, + }, + 'foo-element': { + 'foo-reviewer': 403, 'foo-editor': 200, + 'bar-reviewer': 404, 'bar-editor': 404, + 'example-reviewer': 404, 'example-editor': 404, + }, + 'bar-element': { + 'foo-reviewer': 404, 'foo-editor': 404, + 'bar-reviewer': 403, 'bar-editor': 200, + 'example-reviewer': 404, 'example-editor': 404, + } + }, + 'delete': { + 'all-element': { + 'foo-reviewer': 403, 'foo-editor': 204, + 'bar-reviewer': 403, 'bar-editor': 204, + 'example-reviewer': 403, 'example-editor': 204, + }, + 'foo-element': { + 'foo-reviewer': 403, 'foo-editor': 204, + 'bar-reviewer': 404, 'bar-editor': 404, + 'example-reviewer': 404, 'example-editor': 404, + }, + 'bar-element': { + 'foo-reviewer': 404, 'foo-editor': 404, + 'bar-reviewer': 403, 'bar-editor': 204, + 'example-reviewer': 404, 'example-editor': 404, + } + }, + 'toggle-site': { + 'all-element': { + # foo-editor can not apply own site(foo.com) in test run(example.com) + 'foo-reviewer': 403, 'foo-editor': 403, + # bar-editor can not apply own site(bar.com) in test run(example.com) + 'bar-reviewer': 403, 'bar-editor': 403, + 'example-reviewer': 403, 'example-editor': 200, + }, + 'foo-element': { + 'foo-reviewer': 403, 'foo-editor': 403, + 'bar-reviewer': 403, 'bar-editor': 403, + 'example-reviewer': 403, 'example-editor': 200, + }, + 'bar-element': { + 'foo-reviewer': 403, 'foo-editor': 403, + 'bar-reviewer': 403, 'bar-editor': 403, + 'example-reviewer': 403, 'example-editor': 200, + } + } +} +multisite_users = ( + ('user', 'user'), + ('reviewer', 'reviewer'), + ('editor', 'editor'), + ('example-reviewer', 'example-reviewer'), + ('example-editor', 'example-editor'), + ('foo-user', 'foo-user'), + ('foo-reviewer', 'foo-reviewer'), + ('foo-editor', 'foo-editor'), + ('bar-user', 'bar-user'), + ('bar-reviewer', 'bar-reviewer'), + ('bar-editor', 'bar-editor'), + ('anonymous', None), +) diff --git a/rdmo/core/tests/test_project_status.py b/rdmo/core/tests/test_package_status.py similarity index 59% rename from rdmo/core/tests/test_project_status.py rename to rdmo/core/tests/test_package_status.py index 8eee2433e7..2bd6ca4bf0 100644 --- a/rdmo/core/tests/test_project_status.py +++ b/rdmo/core/tests/test_package_status.py @@ -5,8 +5,13 @@ from django.core.management import call_command import yaml +from packaging.version import Version +def parse_js_version(version_str: str) -> Version: + stripped_version = version_str.lstrip("^~") + return Version(stripped_version) + def test_makemigrations_has_no_changes(db, capsys): call_command("makemigrations", check=True, dry_run=True) captured = capsys.readouterr() @@ -25,11 +30,11 @@ def pre_commit_config(): @pytest.fixture(scope="session") def package_json_versions(package_json): versions = { - "eslint": package_json["devDependencies"]["eslint"], - "eslint-plugin-react": package_json["devDependencies"]["eslint-plugin-react"], - "react": package_json["dependencies"]["react"], + "eslint": parse_js_version(package_json["devDependencies"]["eslint"]), + "eslint-plugin-react": parse_js_version(package_json["devDependencies"]["eslint-plugin-react"]), + "react": parse_js_version(package_json["dependencies"]["react"]), } - return {name: version.replace("^", "") for name, version in versions.items()} + return versions @pytest.fixture(scope="session") def pre_commit_config_versions(pre_commit_config): @@ -41,8 +46,13 @@ def pre_commit_config_versions(pre_commit_config): versions = {} for dependency in sorted(eslint_config["additional_dependencies"]): name, version = dependency.split("@") - versions[name] = version + versions[name] = parse_js_version(version) return versions def test_package_json_and_pre_commit_versions_match(package_json_versions, pre_commit_config_versions): + eslint_plugin_react_package = package_json_versions.pop('eslint-plugin-react') + eslint_plugin_react_precommit = pre_commit_config_versions.pop('eslint-plugin-react') + assert eslint_plugin_react_package.major == eslint_plugin_react_precommit.major + assert abs(eslint_plugin_react_package.minor - eslint_plugin_react_precommit.minor) < 3 + assert package_json_versions == pre_commit_config_versions diff --git a/rdmo/core/tests/test_renderers.py b/rdmo/core/tests/test_renderers.py new file mode 100644 index 0000000000..a57b3417ea --- /dev/null +++ b/rdmo/core/tests/test_renderers.py @@ -0,0 +1,25 @@ +from ..renderers import BaseXMLRenderer + + +class TestRenderer(BaseXMLRenderer): + + def render_document(self, xml, data): + xml.startElement('rdmo', { + 'xmlns:dc': "http://purl.org/dc/elements/1.1/", + 'version': self.version, + 'created': self.created + }) + self.render_text_element(xml, 'text', {}, data['text']) + xml.endElement('rdmo') + + +def test_render(): + renderer = TestRenderer() + xml = renderer.render({'text': 'test'}) + assert 'test' in xml + + +def test_render_ascii_code(): + renderer = TestRenderer() + xml = renderer.render({'text': 'te' + b'\x02'.decode() + 'st'}) + assert 'test' in xml diff --git a/rdmo/core/tests/test_swagger.py b/rdmo/core/tests/test_swagger.py index 83d5cf11a2..f56181845b 100644 --- a/rdmo/core/tests/test_swagger.py +++ b/rdmo/core/tests/test_swagger.py @@ -7,7 +7,7 @@ ) -@pytest.mark.parametrize("username,password", users) +@pytest.mark.parametrize('username,password', users) def test_swagger(db, client, username, password): client.login(username=username, password=password) diff --git a/rdmo/core/tests/test_utils.py b/rdmo/core/tests/test_utils.py index 5964a56d50..05fed48bbb 100644 --- a/rdmo/core/tests/test_utils.py +++ b/rdmo/core/tests/test_utils.py @@ -24,7 +24,7 @@ ) -@pytest.mark.parametrize("url,sanitized_url", urls) +@pytest.mark.parametrize('url,sanitized_url', urls) def test_sanitize_url(url, sanitized_url): assert sanitize_url(url) == sanitized_url @@ -33,6 +33,6 @@ def test_join_url(): assert join_url('https://example.com//', '/terms', 'foo') == 'https://example.com/terms/foo' -@pytest.mark.parametrize("human,bytes", human2bytes_test_values) +@pytest.mark.parametrize('human,bytes', human2bytes_test_values) def test_human2bytes(human: Optional[str], bytes: float): assert human2bytes(human) == bytes diff --git a/rdmo/core/tests/test_views.py b/rdmo/core/tests/test_views.py index 77869e9526..ff8bc45828 100644 --- a/rdmo/core/tests/test_views.py +++ b/rdmo/core/tests/test_views.py @@ -7,6 +7,8 @@ ('reviewer', 'reviewer'), ('user', 'user'), ('api', 'api'), + ('example-editor', 'example-editor'), + ('example-reviewer', 'example-reviewer'), ) @@ -15,7 +17,7 @@ def test_home_anonymous(db, client): assert response.status_code == 200 -@pytest.mark.parametrize("username,password", users) +@pytest.mark.parametrize('username,password', users) def test_home_user(db, client, username, password): client.login(username=username, password=password) response = client.get(reverse('home')) @@ -28,7 +30,7 @@ def test_about_anonymous(db, client): assert response.status_code == 302 -@pytest.mark.parametrize("username,password", users) +@pytest.mark.parametrize('username,password', users) def test_about_user(db, client, username, password): client.login(username=username, password=password) response = client.get(reverse('about')) @@ -53,11 +55,11 @@ def test_i18n_switcher(db, client): assert 'en' in response['Content-Language'] -@pytest.mark.parametrize("username,password", users) +@pytest.mark.parametrize('username,password', users) def test_can_view_management(db, client, username, password): client.login(username=username, password=password) response = client.get(reverse('management')) - if username in ('editor', 'reviewer', 'api'): + if username in ('editor', 'reviewer', 'api', 'example-editor', 'example-reviewer'): assert response.status_code == 200 else: assert response.status_code == 403 diff --git a/rdmo/core/tests/utils.py b/rdmo/core/tests/utils.py new file mode 100644 index 0000000000..3bf4db8c4a --- /dev/null +++ b/rdmo/core/tests/utils.py @@ -0,0 +1,32 @@ +from rdmo.core.models import Model +from rdmo.core.tests.constants import multisite_status_map, status_map_object_permissions + + +def get_obj_perms_status_code(instance, username, method): + ''' looks for the object permissions of the instance and returns the status code ''' + + if isinstance(instance, Model): + try: + if not instance.editors.exists(): + return multisite_status_map[method][username] + except AttributeError as e: + raise AttributeError(f'instance {instance} should have an editors attribute') from e + elif isinstance(instance, str): + pass + + if 'foo-' in str(instance): + instance_obj_perms_key = 'foo-element' + elif 'bar-' in str(instance): + instance_obj_perms_key = 'bar-element' + else: + instance_obj_perms_key = 'all-element' + + try: + method_instance_obj_perms_map = status_map_object_permissions[method][instance_obj_perms_key] + except KeyError as e: + raise KeyError(f'instance ({instance_obj_perms_key}) should be defined in status_map_object_permissions') from e + try: + return method_instance_obj_perms_map[username] + except KeyError: + # not all users are defined in the method_instance_perms_map + return multisite_status_map[method][username] diff --git a/rdmo/core/utils.py b/rdmo/core/utils.py index fa00d680b2..791917b375 100644 --- a/rdmo/core/utils.py +++ b/rdmo/core/utils.py @@ -281,7 +281,7 @@ def render_to_format(request, export_format, title, template_src, context): os.remove(tmp_filename) # create the response object - response = HttpResponse(file_content, content_type='application/%s' % export_format) + response = HttpResponse(file_content, content_type=f'application/{export_format}') response['Content-Disposition'] = content_disposition.encode('utf-8') return response @@ -289,7 +289,7 @@ def render_to_format(request, export_format, title, template_src, context): def render_to_csv(title, rows, delimiter=','): response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="%s.csv"' % title + response['Content-Disposition'] = f'attachment; filename="{title}.csv"' writer = csv.writer(response, delimiter=delimiter) for row in rows: @@ -301,7 +301,7 @@ def render_to_csv(title, rows, delimiter=','): def render_to_json(title, data, delimiter=','): response = HttpResponse(json.dumps(data, indent=2), content_type='text/json') - response['Content-Disposition'] = 'attachment; filename="%s.json"' % title + response['Content-Disposition'] = f'attachment; filename="{title}.json"' return response @@ -372,6 +372,9 @@ def markdown2html(markdown_string): # adoption of the normal markdown function html = markdown(force_str(markdown_string)).strip() + # strip the outer paragraph + html = re.sub(r'^

(.*?)

$',r'\1', html) + # convert `[]{}` to <span title="<title>"><string></span> to allow for underlined tooltips html = re.sub( r'\[(.*?)\]\{(.*?)\}', diff --git a/rdmo/core/views.py b/rdmo/core/views.py index 3ec0ec2a47..3ec0930d7e 100644 --- a/rdmo/core/views.py +++ b/rdmo/core/views.py @@ -1,3 +1,4 @@ +import hashlib import logging from django.conf import settings @@ -31,7 +32,7 @@ def home(request): else: if settings.LOGIN_FORM: if settings.ACCOUNT or settings.SOCIALACCOUNT: - from allauth.account.forms import LoginForm + from rdmo.accounts.forms import LoginForm return render(request, 'core/home.html', { 'form': LoginForm(), 'signup_url': reverse("account_signup") @@ -83,6 +84,18 @@ def get(self, request, *args, **kwargs): return super().get(self, request, *args, **kwargs) +class StoreIdViewMixin(View): + + def render_to_response(self, context, **response_kwargs): + response = super().render_to_response(context, **response_kwargs) + response.set_cookie('storeid', self.get_store_id(), samesite='Lax') + return response + + def get_store_id(self): + session_key = self.request.session.session_key or 'anonymous' + return hashlib.sha256(session_key.encode()).hexdigest() + + class RedirectViewMixin(View): def post(self, request, *args, **kwargs): diff --git a/rdmo/core/xml.py b/rdmo/core/xml.py index 5327969433..264977352e 100644 --- a/rdmo/core/xml.py +++ b/rdmo/core/xml.py @@ -1,41 +1,155 @@ import logging import re +from collections import OrderedDict +from pathlib import Path +from typing import Dict, Optional, Tuple +from xml.etree.ElementTree import Element as xmlElement + +from django.utils.translation import gettext_lazy as _ import defusedxml.ElementTree as ET -from packaging.version import parse - -log = logging.getLogger(__name__) - -models = { - 'catalog': 'questions.catalog', - 'section': 'questions.section', - 'page': 'questions.page', - 'questionset': 'questions.questionset', - 'question': 'questions.question', - 'attribute': 'domain.attribute', - 'optionset': 'options.optionset', - 'option': 'options.option', - 'condition': 'conditions.condition', - 'task': 'tasks.task', - 'view': 'views.view' -} - - -def read_xml_file(file_name): +from packaging.version import Version, parse + +from rdmo import __version__ as RDMO_INSTANCE_VERSION +from rdmo.core.constants import RDMO_MODELS +from rdmo.core.imports import ImportElementFields + +logger = logging.getLogger(__name__) + +LEGACY_RDMO_XML_VERSION = '1.11.0' +ELEMENTS_USING_KEY = {RDMO_MODELS['attribute']} + + +def resolve_file(file_name: str) -> Tuple[Optional[Path], Optional[str]]: + file = Path(file_name).resolve() + if file.exists(): + return file, None + return None, _('This file does not exists.') + + +def read_xml(file: Path) -> Tuple[Optional[xmlElement], Optional[str]]: + # step 2: parse xml and get the root + try: + root = ET.parse(file).getroot() + return root, None + except Exception as e: + return None, _('XML Parsing Error') + f': {e!s}' + + +def validate_root(root: Optional[xmlElement]) -> Tuple[bool, Optional[str]]: + if root is None: + return False, _('The content of the XML file does not consist of well-formed data or markup.') + if root.tag != 'rdmo': + return False, _('This XML does not contain RDMO content.') + return True, None + + +def validate_and_get_xml_version_from_root(root: xmlElement) -> Tuple[Optional[Version], list]: + unparsed_root_version = root.attrib.get('version') or LEGACY_RDMO_XML_VERSION + root_version, rdmo_version = parse(unparsed_root_version), parse(RDMO_INSTANCE_VERSION) + if root_version > rdmo_version: + logger.info('Import failed version validation (%s > %s)', root_version, rdmo_version) + errors = [ + _('This RDMO XML file does not have a valid version number.'), + f'XML Version ({root_version}) is greater than RDMO instance version {rdmo_version}' + ] + return None, errors + return root_version, [] + + +def validate_legacy_elements(elements: dict, root_version: Version) -> list: + + try: + validate_pre_conversion_for_missing_key_in_legacy_elements(elements, root_version) + return [] + except ValueError as e: + logger.info('Import failed with ValueError (%s)', str(e)) + errors = [ + _('XML Parsing Error') + f': {e!s}', + _('This is not a valid RDMO XML file.') + ] + return errors + + +def parse_elements(root: xmlElement) -> Tuple[Dict, Optional[str]]: + # step 3: create element dicts from xml + try: + elements = flat_xml_to_elements(root) + return elements, None + except (KeyError, TypeError, AttributeError) as e: + logger.info('Import failed with %s (%s)', type(e).__name__, e) + return {}, _('This is not a valid RDMO XML file.') + + +def parse_xml_to_elements(xml_file=None) -> Tuple[OrderedDict, list]: + + errors = [] + + file, file_error = resolve_file(xml_file) + if file_error is not None: + logger.error(file_error) + errors.append(file_error) + return OrderedDict(), errors + + root, read_error = read_xml(file) + + if read_error: + logger.error(read_error) + errors.append(read_error) + + # step 2.1: validate the xml root + root_validation, root_validation_error = validate_root(root) + if root_validation is not True: + logger.error('Root element validation failed. %s', root_validation_error) + errors.insert(0, root_validation_error) + return OrderedDict(), errors + + # step 3: create element dicts from xml + elements, parsing_error = parse_elements(root) + if parsing_error is not None: + errors.append(parsing_error) + return OrderedDict(), errors + + # step 3.1: validate version + root_version, version_errors = validate_and_get_xml_version_from_root(root) + if version_errors: + errors.extend(version_errors) + return OrderedDict(), errors + + # step 3.1.1: validate the legacy elements + legacy_errors = validate_legacy_elements(elements, parse(root.attrib.get('version', LEGACY_RDMO_XML_VERSION))) + if legacy_errors: + errors.extend(legacy_errors) + return OrderedDict(), errors + + # step 4: convert elements from previous versions + elements = convert_elements(elements, parse(root.attrib.get('version', LEGACY_RDMO_XML_VERSION))) + + # step 5: order the elements and return + # ordering of elements is done in the import_elements function + + logger.info('XML parsing of %s success (length: %s).', file.name, len(elements)) + + return elements, errors + + +def read_xml_file(file_name, raise_exception=False): try: return ET.parse(file_name).getroot() except Exception as e: - log.error('Xml parsing error: ' + str(e)) + logger.error('Xml file parsing error at getroot: %s', str(e)) + if raise_exception: + raise e from e def parse_xml_string(string): try: return ET.fromstring(string) except Exception as e: - log.error('Xml parsing error: ' + str(e)) + logger.error('Xml parsing from string error: %s', str(e)) -def flat_xml_to_elements(root): +def flat_xml_to_elements(root) -> dict: elements = {} ns_map = get_ns_map(root) uri_attrib = get_ns_tag('dc:uri', ns_map) @@ -45,7 +159,7 @@ def flat_xml_to_elements(root): element = { 'uri': get_uri(node, ns_map), - 'model': models[node.tag] + 'model': RDMO_MODELS[node.tag] } for sub_node in node: @@ -56,8 +170,8 @@ def flat_xml_to_elements(root): element[tag] = { 'uri': sub_node.attrib[uri_attrib] } - if sub_node.tag in models: - element[tag]['model'] = models[sub_node.tag] + if sub_node.tag in RDMO_MODELS: + element[tag]['model'] = RDMO_MODELS[sub_node.tag] elif 'lang' in sub_node.attrib: # this node has the lang attribute! element['{}_{}'.format(tag, sub_node.attrib['lang'])] = sub_node.text @@ -68,8 +182,8 @@ def flat_xml_to_elements(root): sub_element = { 'uri': sub_sub_node.attrib[uri_attrib] } - if sub_sub_node.tag in models: - sub_element['model'] = models[sub_sub_node.tag] + if sub_sub_node.tag in RDMO_MODELS: + sub_element['model'] = RDMO_MODELS[sub_sub_node.tag] if 'order' in sub_sub_node.attrib: sub_element['order'] = sub_sub_node.attrib['order'] @@ -112,23 +226,53 @@ def get_uri(treenode, ns_map): def strip_ns(tag, ns_map): for ns in ns_map.values(): - if tag.startswith('{%s}' % ns): - return tag.replace('{%s}' % ns, '') + if tag.startswith(f'{{{ns}}}'): + return tag.replace(f'{{{ns}}}', '') return tag -def convert_elements(elements, version): - parsed_version = parse('1.11.0') if version is None else parse(version) +def convert_elements(elements, version: Version): + if not isinstance(version, Version): + raise TypeError('Version should be of parsed version type.') - if parsed_version < parse('2.0.0'): + if version < parse('2.0.0'): + validate_pre_conversion_for_missing_key_in_legacy_elements(elements, version) elements = convert_legacy_elements(elements) - if parsed_version < parse('2.1.0'): + if version < parse('2.1.0'): elements = convert_additional_input(elements) return elements +def validate_pre_conversion_for_missing_key_in_legacy_elements(elements, version: Version) -> None: + if version < parse('2.0.0'): + models_in_elements = {i['model'] for i in elements.values()} + if models_in_elements <= ELEMENTS_USING_KEY: + # xml contains only domain.attribute or is empty + return + # inspect the elements for missing 'key' fields + elements_to_inspect = filter(lambda x: x['model'] not in ELEMENTS_USING_KEY, elements.values()) + if not any('key' in el for el in elements_to_inspect): + raise ValueError(f"Missing legacy elements, elements containing 'key' were expected for this XML with version {version} and elements {models_in_elements}.") # noqa: E501 + + +def update_related_legacy_elements(elements: Dict, + target_uri: str, source_model: str, + legacy_element_field: str, element_field: str): + # search for the related elements that use the uri + related_elements = [ + element for element in elements.values() + if element['model'] == source_model + and element.get(legacy_element_field, {}).get('uri') == target_uri + ] + # write the related elements back into the related element + elements[target_uri][element_field] = [ + {k: v for k, v in element.items() if k in ('uri', 'model', 'order')} + for element in related_elements + ] + + def convert_legacy_elements(elements): # first pass: identify pages for uri, element in elements.items(): @@ -147,20 +291,30 @@ def convert_legacy_elements(elements): elif element['model'] == 'questions.catalog': element['uri_path'] = element.pop('key') + # Add sections to the catalog + update_related_legacy_elements(elements, uri, 'questions.section', 'catalog', 'sections') elif element['model'] == 'questions.section': del element['key'] element['uri_path'] = element.pop('path') - - if element.get('catalog') is not None: - element['catalog']['order'] = element.pop('order') + del element['catalog'] # sections do not have catalog anymore + # Add section_pages to the section + update_related_legacy_elements(elements, uri, 'questions.page', 'section', 'pages') elif element['model'] == 'questions.page': del element['key'] element['uri_path'] = element.pop('path') + del element['section'] # pages do not have sections anymore + + # Add page_questionsets to the page + # Add questionsets to the page + update_related_legacy_elements(elements, uri, 'questions.questionset', 'questionset', 'questionsets') - if element.get('section') is not None: - element['section']['order'] = element.pop('order') + # Add page_questions to the page + update_related_legacy_elements(elements, uri, 'questions.question', 'question', 'questions') + + # Add page_conditions to the page + update_related_legacy_elements(elements, uri, 'conditions.condition', 'condition', 'conditions') elif element['model'] == 'questions.questionset': del element['key'] @@ -170,11 +324,15 @@ def convert_legacy_elements(elements): if parent is not None: if elements[parent].get('model') == 'questions.page': # this questionset belongs to a page now - del element['questionset'] - element['page'] = { - 'uri': parent, + parent_questionsets = elements[parent].get('questionset') + parent_questionsets = parent_questionsets or [] + parent_questionsets.append({ + 'uri': element['uri'], + 'model': element['model'], 'order': element.pop('order') - } + }) + elements[parent]['questionset'] = parent_questionsets + del element['questionset'] else: # this questionset still belongs to a questionset element['questionset']['order'] = element.pop('order') @@ -185,26 +343,26 @@ def convert_legacy_elements(elements): parent = element.get('questionset').get('uri') if parent is not None: - if elements[parent].get('model') == 'questions.page': - # this question belongs to a page now - del element['questionset'] - element['page'] = { - 'uri': parent, - 'order': element.pop('order') - } - else: - # this question still belongs to a questionset - element['questionset']['order'] = element.pop('order') + parent_questionsets = elements[parent].get('questions', []) + parent_questionsets.append({ + 'uri': element['uri'], + 'model': element['model'], + 'order': element.pop('order') + }) + elements[parent]['questions'] = parent_questionsets + del element['questionset'] elif element['model'] == 'options.optionset': element['uri_path'] = element.pop('key') + update_related_legacy_elements(elements, uri, 'options.option', 'optionset', 'options') + elif element['model'] == 'options.option': del element['key'] element['uri_path'] = element.pop('path') - if element.get('optionset') is not None: - element['optionset']['order'] = element.pop('order') + del element['optionset'] # options do not have optionsets anymore + if element['model'] == 'tasks.task': element['uri_path'] = element.pop('key') @@ -219,7 +377,9 @@ def convert_additional_input(elements): for uri, element in elements.items(): if element['model'] == 'options.option': additional_input = element.get('additional_input') - if additional_input == 'True': + if additional_input in ['', 'text', 'textarea']: # from Option.ADDITIONAL_INPUT_CHOICES + pass + elif additional_input == 'True': element['additional_input'] = 'text' else: element['additional_input'] = '' @@ -227,29 +387,31 @@ def convert_additional_input(elements): return elements -def order_elements(elements): - ordered_elements = {} - for uri, element in elements.items(): - append_element(ordered_elements, elements, uri, element) +def order_elements(elements: OrderedDict) -> OrderedDict: + ordered_elements = OrderedDict() + for uri, element in reversed(elements.items()): + append_element(ordered_elements, elements, uri, element,) return ordered_elements -def append_element(ordered_elements, unordered_elements, uri, element): +def append_element(ordered_elements, unordered_elements, uri, element) -> None: if element is None: return + for key, element_value in element.items(): + if key in list(ImportElementFields): + continue - for element_value in element.values(): if isinstance(element_value, dict): sub_uri = element_value.get('uri') sub_element = unordered_elements.get(sub_uri) - if sub_uri not in ordered_elements: + if sub_uri not in ordered_elements and sub_uri is not None: append_element(ordered_elements, unordered_elements, sub_uri, sub_element) elif isinstance(element_value, list): for value in element_value: sub_uri = value.get('uri') sub_element = unordered_elements.get(sub_uri) - if sub_uri not in ordered_elements: + if sub_uri not in ordered_elements and sub_uri is not None: append_element(ordered_elements, unordered_elements, sub_uri, sub_element) if uri not in ordered_elements: diff --git a/rdmo/domain/imports.py b/rdmo/domain/imports.py index 5a97f6bcfa..32627133a0 100644 --- a/rdmo/domain/imports.py +++ b/rdmo/domain/imports.py @@ -1,8 +1,7 @@ import logging +from typing import Optional -from django.contrib.sites.models import Site - -from rdmo.core.imports import check_permissions, set_common_fields, set_foreign_field, validate_instance +from rdmo.core.import_helpers import ElementImportHelper, ExtraFieldHelper from .models import Attribute from .validators import AttributeLockedValidator, AttributeParentValidator, AttributeUniqueURIValidator @@ -10,32 +9,22 @@ logger = logging.getLogger(__name__) -def import_attribute(element, save=False, user=None): - try: - attribute = Attribute.objects.get(uri=element.get('uri')) - except Attribute.DoesNotExist: - attribute = Attribute() - - set_common_fields(attribute, element) - - set_foreign_field(attribute, 'parent', element) - - attribute.path = Attribute.build_path(attribute.key, attribute.parent) - - validate_instance(attribute, element, AttributeLockedValidator, - AttributeParentValidator, AttributeUniqueURIValidator) - - check_permissions(attribute, element, user) +def build_attribute_path(instance: Optional[Attribute]=None): + if instance is not None: + return instance.build_path(instance.key, instance.parent) - if save and not element.get('errors'): - if attribute.id: - element['updated'] = True - logger.debug('Attribute %s updated.', element.get('uri')) - else: - element['created'] = True - logger.debug('Attribute created with uri %s.', element.get('uri')) +def build_attribute_uri(instance: Optional[Attribute]=None): + if instance is not None: + return instance.build_uri(instance.uri_prefix, instance.path) - attribute.save() - attribute.editors.add(Site.objects.get_current()) - return attribute +import_helper_attribute = ElementImportHelper( + model=Attribute, + common_fields=('uri_prefix', 'key', 'comment'), + validators=(AttributeLockedValidator, AttributeParentValidator, AttributeUniqueURIValidator), + foreign_fields=('parent',), + extra_fields=( + ExtraFieldHelper(field_name='path', callback=build_attribute_path, overwrite_in_element=True), + ExtraFieldHelper(field_name='uri', callback=build_attribute_uri, overwrite_in_element=True), + ) +) diff --git a/rdmo/domain/renderers/mixins.py b/rdmo/domain/renderers/mixins.py index cc0589cf52..45899ba86e 100644 --- a/rdmo/domain/renderers/mixins.py +++ b/rdmo/domain/renderers/mixins.py @@ -1,6 +1,6 @@ class AttributeRendererMixin: - def render_attribute(self, xml, attribute): + def render_attribute(self, xml, attribute, include_children=True): if attribute['uri'] not in self.uris: self.uris.add(attribute['uri']) @@ -12,6 +12,10 @@ def render_attribute(self, xml, attribute): self.render_text_element(xml, 'parent', {'dc:uri': attribute['parent']}, None) xml.endElement('attribute') - if 'children' in attribute and attribute['children']: - for child in attribute['children']: + if include_children: + for child in attribute.get('children', []): self.render_attribute(xml, child) + + parent_data = attribute.get('parent_data') + if parent_data: + self.render_attribute(xml, parent_data, include_children=False) diff --git a/rdmo/domain/serializers/export.py b/rdmo/domain/serializers/export.py index 27a5bdf16e..7071482140 100644 --- a/rdmo/domain/serializers/export.py +++ b/rdmo/domain/serializers/export.py @@ -6,6 +6,7 @@ class AttributeExportSerializer(serializers.ModelSerializer): parent = serializers.CharField(source='parent.uri', default=None, read_only=True) + parent_data = serializers.SerializerMethodField() class Meta: model = Attribute @@ -15,5 +16,10 @@ class Meta: 'key', 'path', 'comment', - 'parent' + 'parent', + 'parent_data' ) + + def get_parent_data(self, obj): + if obj.parent is not None: + return AttributeExportSerializer(obj.parent).data diff --git a/rdmo/domain/tests/test_admin.py b/rdmo/domain/tests/test_admin.py index 71b2b4ee54..a5408e1f1c 100644 --- a/rdmo/domain/tests/test_admin.py +++ b/rdmo/domain/tests/test_admin.py @@ -1,9 +1,7 @@ from django.urls import reverse -def test_attribute_search(db, client): - client.login(username='admin', password='admin') - +def test_attribute_search(admin_client): url = reverse('admin:domain_attribute_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 diff --git a/rdmo/domain/tests/test_viewset_attribute_multisite.py b/rdmo/domain/tests/test_viewset_attribute_multisite.py index 674469a694..39bd7a663e 100644 --- a/rdmo/domain/tests/test_viewset_attribute_multisite.py +++ b/rdmo/domain/tests/test_viewset_attribute_multisite.py @@ -4,9 +4,10 @@ from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import Attribute from .test_viewset_attribute import urlnames diff --git a/rdmo/locale/de/LC_MESSAGES/django.mo b/rdmo/locale/de/LC_MESSAGES/django.mo index 4ed205f090..69e7615092 100644 Binary files a/rdmo/locale/de/LC_MESSAGES/django.mo and b/rdmo/locale/de/LC_MESSAGES/django.mo differ diff --git a/rdmo/locale/de/LC_MESSAGES/django.po b/rdmo/locale/de/LC_MESSAGES/django.po index e9fef98307..b33dccf249 100644 --- a/rdmo/locale/de/LC_MESSAGES/django.po +++ b/rdmo/locale/de/LC_MESSAGES/django.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: RDMO\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-15 11:37+0100\n" -"PO-Revision-Date: 2023-12-15 11:37+0100\n" +"POT-Creation-Date: 2024-07-23 13:28+0200\n" +"PO-Revision-Date: 2024-07-23 09:56+0200\n" "Last-Translator: Jochen Klar <jklar@aip.de>\n" "Language-Team: RDMO <rdmo-team@listserv.dfn.de>\n" "Language: de\n" @@ -25,7 +25,7 @@ msgstr "Vorname" msgid "Last name" msgstr "Nachname" -#: accounts/forms.py:101 core/constants.py:20 projects/models/invite.py:28 +#: accounts/forms.py:101 core/constants.py:22 projects/models/invite.py:28 msgid "E-mail" msgstr "E-Mail" @@ -85,7 +85,7 @@ msgid "The text for this additional field (in the quinary language)." msgstr "Der Text für das zusätzliche Feld (fünfte Sprache)." #: accounts/models.py:48 options/models.py:216 questions/models/catalog.py:111 -#: questions/models/page.py:111 questions/models/question.py:71 +#: questions/models/page.py:136 questions/models/question.py:71 #: questions/models/questionset.py:106 views/models.py:102 msgid "Help (primary)" msgstr "Hilfe (erste Sprache)" @@ -98,7 +98,7 @@ msgstr "" "Der Hilfetext, der neben dem Eingabefeld angezeigt wird (erste Sprache)." #: accounts/models.py:53 options/models.py:221 questions/models/catalog.py:116 -#: questions/models/page.py:116 questions/models/question.py:76 +#: questions/models/page.py:141 questions/models/question.py:76 #: questions/models/questionset.py:111 views/models.py:107 msgid "Help (secondary)" msgstr "Hilfe (zweite Sprache)" @@ -111,7 +111,7 @@ msgstr "" "Der Hilfetext, der neben dem Eingabefeld angezeigt wird (zweite Sprache)." #: accounts/models.py:58 options/models.py:226 questions/models/catalog.py:121 -#: questions/models/page.py:121 questions/models/question.py:81 +#: questions/models/page.py:146 questions/models/question.py:81 #: questions/models/questionset.py:116 views/models.py:112 msgid "Help (tertiary)" msgstr "Hilfe (dritte Sprache)" @@ -124,7 +124,7 @@ msgstr "" "Der Hilfetext, der neben dem Eingabefeld angezeigt wird (dritte Sprache)." #: accounts/models.py:63 options/models.py:231 questions/models/catalog.py:126 -#: questions/models/page.py:126 questions/models/question.py:86 +#: questions/models/page.py:151 questions/models/question.py:86 #: questions/models/questionset.py:121 views/models.py:117 msgid "Help (quaternary)" msgstr "Hilfe (vierte Sprache)" @@ -137,7 +137,7 @@ msgstr "" "Der Hilfetext, der neben dem Eingabefeld angezeigt wird (vierte Sprache)." #: accounts/models.py:68 options/models.py:236 questions/models/catalog.py:131 -#: questions/models/page.py:131 questions/models/question.py:91 +#: questions/models/page.py:156 questions/models/question.py:91 #: questions/models/questionset.py:126 views/models.py:122 msgid "Help (quinary)" msgstr "Hilfe (fünfte Sprache)" @@ -217,8 +217,7 @@ msgstr "Die Seiten bei denen dieser Benutzer Reviewer ist." #: accounts/models.py:154 projects/models/invite.py:33 #: projects/models/membership.py:30 #: projects/templates/projects/project_detail_invites.html:16 -#: projects/templates/projects/project_detail_memberships.html:21 -#: projects/templates/projects/projects.html:138 +#: projects/templates/projects/project_detail_memberships.html:22 msgid "Role" msgstr "Rolle" @@ -777,15 +776,13 @@ msgstr "" #: accounts/templatetags/accounts_tags.py:19 #: projects/templates/projects/project_questions_modal_form_valuesets.html:19 -#: projects/templates/projects/projects.html:136 -#: projects/templates/projects/site_projects.html:64 msgid "Name" msgstr "Name" #: conditions/apps.py:7 conditions/models.py:90 #: conditions/templates/conditions/export/conditions.html:6 #: options/models.py:62 options/templates/options/export/optionset.html:29 -#: questions/models/page.py:161 questions/models/question.py:221 +#: questions/models/page.py:186 questions/models/question.py:221 #: questions/models/questionset.py:156 tasks/models.py:142 #: tasks/templates/tasks/export/tasks.html:48 msgid "Conditions" @@ -934,44 +931,52 @@ msgstr "Bedingung" msgid "Target" msgstr "Ziel" -#: core/constants.py:14 core/settings.py:318 options/models.py:155 +#: core/constants.py:15 core/settings.py:369 options/models.py:155 #: options/templates/options/export/option.html:8 projects/models/value.py:69 #: tasks/templates/tasks/export/tasks.html:31 msgid "Text" msgstr "Text" -#: core/constants.py:15 projects/models/issue.py:110 +#: core/constants.py:16 projects/models/issue.py:110 msgid "URL" msgstr "URL" -#: core/constants.py:16 +#: core/constants.py:17 msgid "Integer" msgstr "Ganzzahl" -#: core/constants.py:17 +#: core/constants.py:18 msgid "Float" msgstr "Kommazahl" -#: core/constants.py:18 +#: core/constants.py:19 msgid "Boolean" msgstr "Boolsche Variable" -#: core/constants.py:19 +#: core/constants.py:20 +msgid "Date" +msgstr "Datum" + +#: core/constants.py:21 msgid "Datetime" msgstr "Datum und Zeit" -#: core/constants.py:21 +#: core/constants.py:23 msgid "Phone" msgstr "Telefon" -#: core/constants.py:22 options/models.py:272 projects/models/value.py:74 +#: core/constants.py:24 options/models.py:272 projects/models/value.py:74 msgid "Option" msgstr "Option" -#: core/constants.py:23 projects/models/value.py:79 +#: core/constants.py:25 projects/models/value.py:79 msgid "File" msgstr "Datei" +#: core/imports.py:603 +msgid "You have no permissions to import" +msgstr "Sie haben keine Berechtigung zum Importieren" + #: core/models.py:16 msgid "created" msgstr "erstellt" @@ -980,99 +985,124 @@ msgstr "erstellt" msgid "updated" msgstr "zuletzt geändert" -#: core/settings.py:133 +#: core/settings.py:134 msgid "English" msgstr "Englisch" -#: core/settings.py:134 +#: core/settings.py:135 msgid "German" msgstr "Deutsch" -#: core/settings.py:253 +#: core/settings.py:254 msgid "PDF" msgstr "PDF" -#: core/settings.py:254 +#: core/settings.py:255 msgid "Rich Text Format" msgstr "Rich Text Format" -#: core/settings.py:255 +#: core/settings.py:256 msgid "Open Office" msgstr "Open Office" -#: core/settings.py:256 +#: core/settings.py:257 msgid "Microsoft Office" msgstr "Microsoft Office" -#: core/settings.py:257 +#: core/settings.py:258 msgid "HTML" msgstr "HTML" -#: core/settings.py:258 +#: core/settings.py:259 msgid "Markdown" msgstr "Markdown" -#: core/settings.py:259 +#: core/settings.py:260 msgid "mediawiki" msgstr "mediawiki" -#: core/settings.py:260 +#: core/settings.py:261 msgid "LaTeX" msgstr "LaTeX" -#: core/settings.py:282 core/settings.py:289 +#: core/settings.py:283 core/settings.py:290 msgid "RDMO XML" msgstr "RDMO XML" -#: core/settings.py:283 +#: core/settings.py:284 msgid "CSV (comma separated)" msgstr "CSV (Komma getrennt)" -#: core/settings.py:284 +#: core/settings.py:285 msgid "CSV (semicolon separated)" msgstr "CSV (Semikolon getrennt)" -#: core/settings.py:285 +#: core/settings.py:286 msgid "JSON" msgstr "JSON" -#: core/settings.py:319 options/models.py:156 +#: core/settings.py:323 +msgid "Enter a valid integer." +msgstr "Geben Sie eine gültige Ganzzahl ein." + +#: core/settings.py:327 +msgid "Enter a valid float." +msgstr "Geben Sie einen gültigen Wert ein." + +#: core/settings.py:343 +msgid "Enter a valid boolean (e.g. 0, 1)." +msgstr "Geben Sie einen gültigen booleschen Wert ein (z. B. 0, 1)." + +#: core/settings.py:347 +msgid "" +"Enter a valid date (e.g. \"02.03.2024\", \"03/02/2024\", \"2024-02-03\")." +msgstr "" +"Geben Sie ein gültiges Datum ein (z.B. \"02.03.2024\", \"03/02/2024\", " +"\"2024-02-03\")." + +#: core/settings.py:361 +msgid "Enter a valid phone number (e.g. \"123456\" or \"+49 (0) 30 123456\")." +msgstr "" +"Geben Sie eine gültige Telefonnummer ein (z. B. \"123456\" oder \"+49 (0) 30 " +"123456\")." + +#: core/settings.py:370 options/models.py:156 msgid "Textarea" msgstr "Textfeld" -#: core/settings.py:320 +#: core/settings.py:371 msgid "Yes/No" msgstr "Ja/Nein" -#: core/settings.py:321 +#: core/settings.py:372 msgid "Checkboxes" msgstr "Ankreuzfelder" -#: core/settings.py:322 +#: core/settings.py:373 msgid "Radio buttons" msgstr "Radio Buttons" -#: core/settings.py:323 +#: core/settings.py:374 msgid "Select drop-down" msgstr "Select-Dropdown-Menü" -#: core/settings.py:324 +#: core/settings.py:375 msgid "Autocomplete" msgstr "Autocomplete" -#: core/settings.py:325 +#: core/settings.py:376 msgid "Free autocomplete" msgstr "Freies Autocomplete" -#: core/settings.py:326 +#: core/settings.py:377 msgid "Range slider" msgstr "Schieberegler" -#: core/settings.py:327 +#: core/settings.py:378 msgid "Date picker" msgstr "Datumspicker" -#: core/settings.py:328 +#: core/settings.py:379 msgid "File upload" msgstr "Dateien-Upload" @@ -1111,6 +1141,7 @@ msgstr "Leider ist bei der angeforderten URL ein Fehler aufgetreten." #: core/templates/core/about_text_de.html:6 #: core/templates/core/about_text_en.html:6 +#: core/templates/core/about_text_es.html:6 #: core/templates/core/about_text_fr.html:6 #: core/templates/core/about_text_it.html:6 msgid "RDMO Logo" @@ -1196,11 +1227,11 @@ msgstr "Das Fragen und Fragensets für dieses Fragenset." msgid "This format is not supported." msgstr "Dieses Format wird nicht unterstützt." -#: core/utils.py:383 +#: core/utils.py:386 msgid "show more" msgstr "mehr anzeigen" -#: core/utils.py:384 +#: core/utils.py:387 msgid "show less" msgstr "weniger anzeigen" @@ -1209,8 +1240,8 @@ msgstr "weniger anzeigen" msgid "%(model)s with the uri \"%(uri)s\" already exists." msgstr "%(model)s mit der uri \"%(uri)s\" existiert bereits." -#: core/validators.py:80 domain/validators.py:14 projects/forms.py:349 -#: projects/forms.py:370 +#: core/validators.py:80 domain/validators.py:14 projects/forms.py:357 +#: projects/forms.py:378 msgid "This field is required." msgstr "Dieses Feld wird benötigt." @@ -1230,6 +1261,33 @@ msgstr "Ein übergeordnetes Element ist gesperrt." msgid "The element is locked." msgstr "Das Element ist gesperrt." +#: core/xml.py:27 +msgid "This file does not exists." +msgstr "Diese Datei existiert nicht." + +#: core/xml.py:36 core/xml.py:68 +msgid "XML Parsing Error" +msgstr "XML-Parsing-Fehler" + +#: core/xml.py:41 +msgid "" +"The content of the XML file does not consist of well-formed data or markup." +msgstr "" +"Der Inhalt der XML-Datei besteht nicht aus korrekt formatierten Daten oder " +"Markup." + +#: core/xml.py:43 +msgid "This XML does not contain RDMO content." +msgstr "Die XML-Datei enthält keine RDMO-Daten." + +#: core/xml.py:53 +msgid "This RDMO XML file does not have a valid version number." +msgstr "Diese RDMO-XML-Datei hat keine gültige Versionsnummer." + +#: core/xml.py:69 core/xml.py:81 +msgid "This is not a valid RDMO XML file." +msgstr "Dies ist keine valide RDMO-XML Datei." + #: domain/apps.py:7 domain/templates/domain/export/attributes.html:6 msgid "Domain" msgstr "Domäne" @@ -1307,16 +1365,6 @@ msgstr "" "Ein Attribut kann nicht verschoben werden, um ein Unterattribut von sich " "selbst oder einem seiner Unterattribute zu sein." -#: management/management/commands/import.py:20 management/viewsets.py:63 -msgid "" -"The content of the xml file does not consist of well formed data or markup." -msgstr "" -"Der Inhalt der hochgeladenen XML-Datei entspricht nicht den Erwartungen." - -#: management/management/commands/import.py:22 -msgid "This XML does not contain RDMO content." -msgstr "Die XML-Datei enthält keine RDMO-Daten." - #: management/templates/management/import.html:10 #: management/templates/management/upload.html:13 #, python-format @@ -1365,16 +1413,11 @@ msgstr "Import" msgid "Import elements" msgstr "Elemente importieren" -#: management/viewsets.py:54 management/viewsets.py:104 +#: management/viewsets.py:41 management/viewsets.py:78 msgid "This field may not be blank." msgstr "Dieses Feld darf nicht leer sein." -#: management/viewsets.py:71 management/viewsets.py:74 -#: management/viewsets.py:77 -msgid "This is not a valid RDMO XML file." -msgstr "Dies ist keine valide RDMO-XML Datei." - -#: management/viewsets.py:106 +#: management/viewsets.py:80 msgid "This is not a valid RDMO import JSON." msgstr "Dies ist keine valide RDMO-JSON-Import." @@ -1387,8 +1430,6 @@ msgstr "Dies ist keine valide RDMO-JSON-Import." #: projects/templates/projects/project_detail_integrations.html:19 #: projects/templates/projects/project_detail_sidebar.html:11 #: projects/templates/projects/project_view.html:42 -#: projects/templates/projects/projects.html:29 -#: projects/templates/projects/site_projects.html:48 #: questions/templates/questions/export/question.html:30 msgid "Options" msgstr "Optionen" @@ -1588,11 +1629,11 @@ msgstr "Ansichtstext" msgid "Overlays" msgstr "Einblendungen" -#: overlays/models.py:11 projects/forms.py:182 +#: overlays/models.py:11 projects/forms.py:190 #: projects/models/continuation.py:18 projects/models/invite.py:23 #: projects/models/membership.py:25 projects/models/project.py:33 #: projects/templates/projects/project_detail_invites.html:14 -#: projects/templates/projects/project_detail_memberships.html:19 +#: projects/templates/projects/project_detail_memberships.html:20 msgid "User" msgstr "Benutzer" @@ -1616,7 +1657,7 @@ msgstr "Url-Name" msgid "The url_name for this overlay." msgstr "Der url_name für diese Einblendung." -#: overlays/models.py:26 projects/forms.py:337 +#: overlays/models.py:26 projects/forms.py:345 #: projects/templates/projects/project_answers.html:13 #: projects/templates/projects/project_view.html:28 msgid "Current" @@ -1669,19 +1710,19 @@ msgstr "Autor" msgid "Guest" msgstr "Gast" -#: projects/forms.py:181 +#: projects/forms.py:189 msgid "Username or e-mail" msgstr "Benutzername oder E-Mail" -#: projects/forms.py:183 +#: projects/forms.py:191 msgid "The username or e-mail of the new user." msgstr "Der Benutzername oder die E-Mail des neuen Benutzers." -#: projects/forms.py:195 +#: projects/forms.py:203 msgid "Add member silently" msgstr "Mitglied stillschweigend hinzufügen" -#: projects/forms.py:196 +#: projects/forms.py:204 msgid "" "As site manager or admin, you can directly add users without notifying them " "via e-mail, when you check the following checkbox." @@ -1689,11 +1730,11 @@ msgstr "" "Als Site-Manager oder Admin können Sie Benutzer direkt hinzufügen, ohne sie " "per E-Mail zu benachrichtigen, wenn Sie das folgende Kästchen aktivieren." -#: projects/forms.py:211 projects/serializers/v1/__init__.py:159 +#: projects/forms.py:219 projects/serializers/v1/__init__.py:163 msgid "The user is already a member of the project." msgstr "Der Benutzer ist bereits ein Mitglied dieses Projektes." -#: projects/forms.py:223 +#: projects/forms.py:231 msgid "" "A user with this username or e-mail was not found. Only registered users can " "be invited." @@ -1701,71 +1742,71 @@ msgstr "" "Ein Benutzer mit diesem Benutzernamen oder dieser E-Mail wurde nicht " "gefunden. Nur registrierte Benutzer können eingeladen werden." -#: projects/forms.py:228 +#: projects/forms.py:236 msgid "Only existing users can be added silently." msgstr "Nur bestehende Benutzer können stillschweigend hinzugefügt werden." -#: projects/forms.py:303 projects/forms.py:307 +#: projects/forms.py:311 projects/forms.py:315 #, python-format msgid "Attach %s" msgstr "%s hinzufügen" -#: projects/forms.py:313 +#: projects/forms.py:321 msgid "Subject" msgstr "Betreff" -#: projects/forms.py:314 +#: projects/forms.py:322 msgid "Message" msgstr "Nachricht" -#: projects/forms.py:321 +#: projects/forms.py:329 msgid "Answers" msgstr "Antworten" -#: projects/forms.py:322 +#: projects/forms.py:330 msgid "Attach the output of \"View answers\"." msgstr "Die Ausgabe von \"Antworten anzeigen\" anhängen." -#: projects/forms.py:325 projects/models/project.py:63 +#: projects/forms.py:333 projects/models/project.py:63 #: projects/templates/projects/project_detail_views.html:11 views/apps.py:7 #: views/models.py:136 views/templates/views/export/views.html:6 msgid "Views" msgstr "Ansichten" -#: projects/forms.py:329 +#: projects/forms.py:337 msgid "Files" msgstr "Dateien" -#: projects/forms.py:336 projects/models/snapshot.py:33 +#: projects/forms.py:344 projects/models/snapshot.py:33 #: projects/models/value.py:38 #: projects/templates/projects/project_detail_snapshots.html:20 #: projects/templates/projects/project_import.html:93 msgid "Snapshot" msgstr "Snapshot" -#: projects/forms.py:340 +#: projects/forms.py:348 msgid "Format" msgstr "Format" -#: projects/forms.py:355 projects/forms.py:360 +#: projects/forms.py:363 projects/forms.py:368 msgid "Recipients" msgstr "Empfänger" -#: projects/forms.py:361 +#: projects/forms.py:369 msgid "Enter recipients line by line" msgstr "Empfänger Zeile für Zeile eingeben" -#: projects/imports.py:224 +#: projects/imports.py:235 msgid "Import project from this URL" msgstr "Projekt von dieser URL importieren" -#: projects/mixins.py:79 projects/mixins.py:96 projects/mixins.py:110 -#: projects/mixins.py:133 projects/mixins.py:151 projects/mixins.py:166 -#: projects/mixins.py:201 +#: projects/mixins.py:79 projects/mixins.py:96 projects/mixins.py:111 +#: projects/mixins.py:134 projects/mixins.py:152 projects/mixins.py:167 +#: projects/mixins.py:202 msgid "Import error" msgstr "Fehler beim Import" -#: projects/mixins.py:80 projects/mixins.py:152 projects/mixins.py:202 +#: projects/mixins.py:80 projects/mixins.py:153 projects/mixins.py:203 msgid "There has been an error with your import." msgstr "Bei Ihrem Import ist ein Fehler aufgetreten." @@ -1777,7 +1818,7 @@ msgstr "" "Bei Ihrem Import ist ein Fehler aufgetreten. Es konnte keine hochgeladene " "oder abgerufene Datei gefunden werden." -#: projects/mixins.py:134 +#: projects/mixins.py:135 msgid "Files of this type cannot be imported." msgstr "Dateien dieses Typs können nicht importiert werden." @@ -1797,7 +1838,7 @@ msgstr "Das Projekt für diese Fortsetzung." msgid "The user for this continuation." msgstr "Der Benutzer für diese Fortsetzung." -#: projects/models/continuation.py:23 questions/models/page.py:167 +#: projects/models/continuation.py:23 questions/models/page.py:192 #: questions/templates/questions/export/page.html:3 msgid "Page" msgstr "Seite" @@ -2245,28 +2286,28 @@ msgid "The secret for a webhook to close a task (optional)." msgstr "" "Das Geheimnis für einen Webhook zum Schließen einer Aufgabe (optional)." -#: projects/serializers/v1/__init__.py:164 +#: projects/serializers/v1/__init__.py:168 msgid "A user with that e-mail is already a member of the project." msgstr "" "Ein Benutzer mit dieser E-Mail-Adresse ist bereits Mitglied des Projekts." -#: projects/serializers/v1/__init__.py:172 +#: projects/serializers/v1/__init__.py:176 msgid "Either user or e-mail needs to be provided." msgstr "Es muss entweder ein Benutzer oder eine E-Mail angegeben werden." -#: projects/serializers/v1/__init__.py:174 +#: projects/serializers/v1/__init__.py:178 msgid "User and e-mail are mutually exclusive." msgstr "Benutzer und E-Mail schließen einander aus." -#: projects/serializers/v1/page.py:97 +#: projects/serializers/v1/page.py:98 msgid "entry" msgstr "Eintrag" -#: projects/serializers/v1/page.py:133 +#: projects/serializers/v1/page.py:134 msgid "block" msgstr "Block" -#: projects/serializers/v1/page.py:186 +#: projects/serializers/v1/page.py:187 msgid "set" msgstr "Set" @@ -2436,7 +2477,7 @@ msgstr "Anhänge" #: projects/templates/projects/membership_confirm_delete.html:8 #: projects/templates/projects/membership_confirm_delete.html:17 -#: projects/templates/projects/project_detail_memberships.html:63 +#: projects/templates/projects/project_detail_memberships.html:65 msgid "Delete membership" msgstr "Mitgliedschaft entfernen" @@ -2451,7 +2492,7 @@ msgstr "" #: projects/templates/projects/membership_form.html:9 #: projects/templates/projects/membership_form.html:11 -#: projects/templates/projects/project_detail_memberships.html:53 +#: projects/templates/projects/project_detail_memberships.html:55 msgid "Update membership" msgstr "Mitgliedschaft bearbeiten" @@ -2560,7 +2601,7 @@ msgstr "Nachfolgende Projekte bleiben erhalten." #: projects/templates/projects/project_confirm_leave.html:7 #: projects/templates/projects/project_confirm_leave.html:19 -#: projects/templates/projects/project_detail_memberships.html:59 +#: projects/templates/projects/project_detail_memberships.html:61 #: projects/templates/projects/project_detail_sidebar.html:36 msgid "Leave project" msgstr "Projekt verlassen" @@ -2616,7 +2657,7 @@ msgstr "" "diese zu nutzen." #: projects/templates/projects/project_detail_invites.html:15 -#: projects/templates/projects/project_detail_memberships.html:20 +#: projects/templates/projects/project_detail_memberships.html:21 msgid "E-Mail" msgstr "E-Mail" @@ -2652,16 +2693,16 @@ msgstr "" "generiert. Auf der Seite jeder Aufgabe können Sie sehen, welche Ihrer " "Antworten zur Aktivierung der Aufgabe führen." -#: projects/templates/projects/project_detail_memberships.html:13 +#: projects/templates/projects/project_detail_memberships.html:14 msgid "Members" msgstr "Mitgliedschaften" -#: projects/templates/projects/project_detail_memberships.html:24 +#: projects/templates/projects/project_detail_memberships.html:25 #: projects/templates/projects/project_detail_sidebar.html:78 msgid "Add member" msgstr "Mitglied hinzufügen" -#: projects/templates/projects/project_detail_memberships.html:44 +#: projects/templates/projects/project_detail_memberships.html:46 msgid "of" msgstr "von" @@ -2721,7 +2762,6 @@ msgid "Import values" msgstr "Werte importieren" #: projects/templates/projects/project_detail_sidebar.html:131 -#: projects/templates/projects/projects.html:78 msgid "Import from file" msgstr "Importieren aus Datei" @@ -2730,14 +2770,12 @@ msgid "Import from parent project" msgstr "Importieren aus übergeordnetem Projekt" #: projects/templates/projects/project_detail_sidebar.html:147 -#: projects/templates/projects/projects.html:87 msgid "Import directly" msgstr "Projekt direkt importieren" #: projects/templates/projects/project_detail_snapshots.html:22 #: projects/templates/projects/project_import.html:34 #: projects/templates/projects/project_import.html:104 -#: projects/templates/projects/site_projects.html:66 msgid "Created" msgstr "Erstellt" @@ -2828,7 +2866,6 @@ msgid "Save project" msgstr "Projekt speichern" #: projects/templates/projects/project_form.html:43 -#: projects/templates/projects/projects.html:36 msgid "Create new project" msgstr "Neues Projekt erstellen" @@ -2980,7 +3017,6 @@ msgid "Please select" msgstr "Bitte auswählen" #: projects/templates/projects/project_questions_head.html:5 -#: projects/templates/projects/projects.html:131 msgid "My Projects" msgstr "Meine Projekte" @@ -3003,7 +3039,12 @@ msgstr "Entfernen" msgid "Please give the tab a meaningful name." msgstr "Bitte geben Sie dem Tab einen aus­sa­ge­kräf­tigen Namen." -#: projects/templates/projects/project_questions_navigation.html:23 +#: projects/templates/projects/project_questions_navigation.html:13 +#, python-format +msgid "(%(section_count)s of %(section_total)s)" +msgstr "(%(section_count)s von %(section_total)s)" + +#: projects/templates/projects/project_questions_navigation.html:31 #, python-format msgid "(%(page_count)s of %(page_total)s)" msgstr "(%(page_count)s von %(page_total)s)" @@ -3027,7 +3068,6 @@ msgid "Reload page" msgstr "Seite neu laden" #: projects/templates/projects/project_questions_overview.html:17 -#: projects/templates/projects/site_projects.html:52 msgid "Back to my projects" msgstr "Zurück zu meinen Projekten" @@ -3038,7 +3078,7 @@ msgstr "Zurück zu meinen Projekten" msgid "%(count)s of %(total)s" msgstr "%(count)s von %(total)s" -#: projects/templates/projects/project_questions_question_label.html:5 +#: projects/templates/projects/project_questions_question_label.html:4 msgid "(optional)" msgstr "(optional)" @@ -3095,8 +3135,6 @@ msgid "Overview" msgstr "Übersicht" #: projects/templates/projects/project_questions_sidebar.html:7 -#: projects/templates/projects/projects.html:137 -#: projects/templates/projects/site_projects.html:65 msgid "Progress" msgstr "Fortschritt" @@ -3125,12 +3163,12 @@ msgstr "" "fortzufahren, aber Ihre Eingaben gehen dabei verloren." #: projects/templates/projects/project_questions_value_errors.html:18 -#: projects/viewsets.py:400 +#: projects/viewsets.py:469 msgid "You reached the file quota for this project." msgstr "Sie haben die Quota für dieses Projekt erreicht." #: projects/templates/projects/project_view.html:90 -#: projects/views/project.py:207 +#: projects/views/project.py:129 msgid "Error" msgstr "Fehler" @@ -3140,46 +3178,6 @@ msgstr "" "Beim Erstellen der Ansicht ist ein Fehler aufgetreten. Bitte kontaktieren " "Sie den Support." -#: projects/templates/projects/projects.html:47 -#, python-format -msgid "View all projects on %(site)s" -msgstr "Alle Projekte auf %(site)s anzeigen" - -#: projects/templates/projects/projects.html:54 -#: projects/templates/projects/site_projects.html:16 -msgid "Filter projects" -msgstr "Projekte filtern" - -#: projects/templates/projects/projects.html:57 -#: projects/templates/projects/site_projects.html:19 -msgid "Search project title" -msgstr "Suche nach Projekttitel" - -#: projects/templates/projects/projects.html:64 -#: projects/templates/projects/site_projects.html:26 -#, python-format -msgid "" -"%(number_of_filtered_projects)s of %(number_of_projects)s projects shown" -msgstr "" -"%(number_of_filtered_projects)s von %(number_of_projects)s Projekte angezeigt" - -#: projects/templates/projects/projects.html:73 -msgid "Import existing project" -msgstr "Vorhandenes Projekt importieren" - -#: projects/templates/projects/projects.html:106 -msgid "Pending invitations" -msgstr "Ausstehende Einladungen" - -#: projects/templates/projects/projects.html:122 -msgid "Click on one of the links to join the projects." -msgstr "Klicken Sie auf einen der Links, um an den Projekten teilzunehmen." - -#: projects/templates/projects/projects.html:139 -#: projects/templates/projects/site_projects.html:67 -msgid "Last changed" -msgstr "Letzte Änderung" - #: projects/templates/projects/projects_pagination.html:7 #: projects/templates/projects/projects_pagination.html:9 msgid "Previous" @@ -3195,15 +3193,6 @@ msgstr "Seite %(number)s von %(num_pages)s." msgid "Next" msgstr "Nächste" -#: projects/templates/projects/site_projects.html:32 -msgid "All catalogs" -msgstr "Alle Kataloge" - -#: projects/templates/projects/site_projects.html:59 -#, python-format -msgid "All projects on %(site)s" -msgstr "Alle Projekte auf %(site)s" - #: projects/templates/projects/snapshot_form.html:15 msgid "Create new snapshot" msgstr "Neuen Snapshot erstellen" @@ -3226,11 +3215,11 @@ msgstr "Zurücksetzen" msgid "(%(progress)s progress)" msgstr "(%(progress)s Fortschritt)" -#: projects/validators.py:27 +#: projects/validators.py:38 msgid "A newer version of this value was found." msgstr "Eine neuere Version dieses Wertes wurde gefunden." -#: projects/validators.py:44 +#: projects/validators.py:62 msgid "" "An existing value for this attribute/set_prefix/set_index/collection_index " "was found." @@ -3238,20 +3227,24 @@ msgstr "" "Es wurde ein vorhandener Wert für dieses Attribut/set_prefix/set_index/" "collection_index gefunden." -#: projects/validators.py:57 +#: projects/validators.py:76 msgid "The file quota for this project has been reached." msgstr "Sie haben die Quota für dieses Projekt erreicht." -#: projects/views/project.py:187 +#: projects/validators.py:126 +msgid "Enter a valid datetime." +msgstr "Geben Sie ein gültiges Datum ein." + +#: projects/views/project.py:109 msgid "Sorry, your invitation has been expired." msgstr "Entschuldigung, Ihre Einladung ist abgelaufen." -#: projects/views/project.py:190 +#: projects/views/project.py:112 #, python-format msgid "Sorry, but this invitation is for the user \"%s\"." msgstr "Entschuldigung, aber diese Einladung ist für den Benutzer \"%s\"." -#: projects/views/project.py:204 +#: projects/views/project.py:126 msgid "Sorry, the invitation link is not valid." msgstr "Der Einladungslink ist leider nicht gültig." @@ -3284,7 +3277,7 @@ msgstr "" msgid "The position of this catalog in lists." msgstr "Die Position von diesem Katalog in Listen." -#: questions/models/catalog.py:66 questions/models/section.py:96 +#: questions/models/catalog.py:66 questions/models/section.py:121 msgid "Sections" msgstr "Abschnitte" @@ -3469,76 +3462,126 @@ msgstr "Der Titel für diese Seite (vierte Sprache)." msgid "The title for this page (in the quinary language)." msgstr "Der Titel für diese Seite (fünfte Sprache)." +#: questions/models/page.py:111 questions/models/section.py:94 +msgid "Short title (primary)" +msgstr "Kurzer Titel (erste Sprache)" + #: questions/models/page.py:112 +msgid "" +"The short title for this page (in the primary language), used in the " +"navigation." +msgstr "Der kurze Titel für diese Seite (erste Sprache)." + +#: questions/models/page.py:116 questions/models/section.py:99 +msgid "Short title (secondary)" +msgstr "Kurzer Titel (zweite Sprache)" + +#: questions/models/page.py:117 +msgid "" +"The short title for this page (in the secondary language), used in the " +"navigation." +msgstr "Der kurze Titel für diese Seite (zweite Sprache)." + +#: questions/models/page.py:121 questions/models/section.py:104 +msgid "Short title (tertiary)" +msgstr "Kurzer Titel (dritte Sprache)" + +#: questions/models/page.py:122 +msgid "" +"The short title for this page (in the tertiary language), used in the " +"navigation." +msgstr "Der kurze Titel für diese Seite (dritte Sprache)." + +#: questions/models/page.py:126 questions/models/section.py:109 +msgid "Short title (quaternary)" +msgstr "Kurzer Titel (vierte Sprache)" + +#: questions/models/page.py:127 +msgid "" +"The short title for this page (in the quaternary language), used in the " +"navigation." +msgstr "Der kurze Titel für diese Seite (vierte Sprache)." + +#: questions/models/page.py:131 questions/models/section.py:114 +msgid "Short title (quinary)" +msgstr "Kurzer Titel (fünfte Sprache)" + +#: questions/models/page.py:132 +msgid "" +"The short title for this page (in the quinary language), used in the " +"navigation." +msgstr "Der kurze Titel für diese Seite (fünfte Sprache)." + +#: questions/models/page.py:137 msgid "The help text for this page (in the primary language)." msgstr "Der Hilfetext für diese Seite (erste Sprache)." -#: questions/models/page.py:117 +#: questions/models/page.py:142 msgid "The help text for this page (in the secondary language)." msgstr "Der Hilfetext für diese Seite (zweite Sprache)." -#: questions/models/page.py:122 +#: questions/models/page.py:147 msgid "The help text for this page (in the tertiary language)." msgstr "Der Hilfetext für diese Seite (dritte Sprache)." -#: questions/models/page.py:127 +#: questions/models/page.py:152 msgid "The help text for this page (in the quaternary language)." msgstr "Der Hilfetext für diese Seite (vierte Sprache)." -#: questions/models/page.py:132 +#: questions/models/page.py:157 msgid "The help text for this page (in the quinary language)." msgstr "Der Hilfetext für diese Seite (fünfte Sprache)." -#: questions/models/page.py:136 questions/models/question.py:156 +#: questions/models/page.py:161 questions/models/question.py:156 #: questions/models/questionset.py:131 msgid "Name (primary)" msgstr "Name (erste Sprache)" -#: questions/models/page.py:137 +#: questions/models/page.py:162 msgid "The name displayed for this page (in the primary language)." msgstr "Der Name, der für diese Seite angezeigt wird (erste Sprache)." -#: questions/models/page.py:141 questions/models/question.py:161 +#: questions/models/page.py:166 questions/models/question.py:161 #: questions/models/questionset.py:136 msgid "Name (secondary)" msgstr "Name (zweite Sprache)" -#: questions/models/page.py:142 +#: questions/models/page.py:167 msgid "The name displayed for this page (in the secondary language)." msgstr "Der Name, der für diese Seite angezeigt wird (zweite Sprache)." -#: questions/models/page.py:146 questions/models/question.py:166 +#: questions/models/page.py:171 questions/models/question.py:166 #: questions/models/questionset.py:141 msgid "Name (tertiary)" msgstr "Name (dritte Sprache)" -#: questions/models/page.py:147 +#: questions/models/page.py:172 msgid "The name displayed for this page (in the tertiary language)." msgstr "Der Name, der für diese Seite angezeigt wird (dritte Sprache)." -#: questions/models/page.py:151 questions/models/question.py:171 +#: questions/models/page.py:176 questions/models/question.py:171 #: questions/models/questionset.py:146 msgid "Name (quaternary)" msgstr "Name (vierte Sprache)" -#: questions/models/page.py:152 +#: questions/models/page.py:177 msgid "The name displayed for this page (in the quaternary language)." msgstr "Der Name, der für diese Seite angezeigt wird (vierte Sprache)." -#: questions/models/page.py:156 questions/models/question.py:176 +#: questions/models/page.py:181 questions/models/question.py:176 #: questions/models/questionset.py:151 msgid "Name (quinary)" msgstr "Name (fünfte Sprache)" -#: questions/models/page.py:157 +#: questions/models/page.py:182 msgid "The name displayed for this page (in the quinary language)." msgstr "Der Name, der für diese Seite angezeigt wird (fünfte Sprache)." -#: questions/models/page.py:162 +#: questions/models/page.py:187 msgid "List of conditions evaluated for this page." msgstr "Liste der Bedingungen die für diese Seite ausgewertet werden." -#: questions/models/page.py:168 questions/models/section.py:59 +#: questions/models/page.py:193 questions/models/section.py:59 msgid "Pages" msgstr "Seiten" @@ -3934,25 +3977,55 @@ msgid "The title for this section (in the quinary language)." msgstr "Der Titel für diesen Abschnitt (fünfte Sprache)." #: questions/models/section.py:95 +msgid "" +"The short title for this section (in the primary language), used in the " +"navigation." +msgstr "Der kurze Titel für diesen Abschnitt (erste Sprache)." + +#: questions/models/section.py:100 +msgid "" +"The short title for this section (in the secondary language), used in the " +"navigation." +msgstr "Der kurze Titel für diesen Abschnitt (zweite Sprache)." + +#: questions/models/section.py:105 +msgid "" +"The short title for this section (in the tertiary language), used in the " +"navigation." +msgstr "Der kurze Titel für diesen Abschnitt (dritte Sprache)." + +#: questions/models/section.py:110 +msgid "" +"The short title for this section (in the quaternary language), used in the " +"navigation." +msgstr "Der kurze Titel für diesen Abschnitt (vierte Sprache)." + +#: questions/models/section.py:115 +msgid "" +"The short title for this section (in the quinary language), used in the " +"navigation." +msgstr "Der kurze Titel für diesen Abschnitt (fünfte Sprache)." + +#: questions/models/section.py:120 #: questions/templates/questions/export/section.html:3 msgid "Section" msgstr "Abschnitt" -#: questions/serializers/v1/question.py:110 +#: questions/serializers/v1/question.py:114 msgid "" "If the \"Checkboxes\" widget is used, \"is_collection\" must be checked." msgstr "" "Wenn das Widget \"Ankreuzfelder\" verwendet wird, muss Ist eine Sammlung\" " "angekreuzt werden." -#: questions/serializers/v1/question.py:116 +#: questions/serializers/v1/question.py:120 msgid "" "If the \"Date picker\" widget is used, the value type must be \"Datetime\"." msgstr "" "Wenn das Widget \"Datumspicker\" verwendet wird, muss der Wertetyp \"Datum " "und Zeit\" sein." -#: questions/serializers/v1/question.py:122 +#: questions/serializers/v1/question.py:126 msgid "If the \"Yes/No\" widget is used, the value type must be \"Boolean\"." msgstr "" "Wenn das Widget \"Ja/Nein\" verwendet wird, muss der Wertetyp \"Boolsche " @@ -4260,10 +4333,46 @@ msgstr "Hilfetext" msgid "file" msgstr "Datei" -#: views/templatetags/view_tags.py:184 +#: views/templatetags/view_tags.py:202 msgid "Set" msgstr "Set" +#, python-format +#~ msgid "View all projects on %(site)s" +#~ msgstr "Alle Projekte auf %(site)s anzeigen" + +#~ msgid "Filter projects" +#~ msgstr "Projekte filtern" + +#~ msgid "Search project title" +#~ msgstr "Suche nach Projekttitel" + +#, python-format +#~ msgid "" +#~ "%(number_of_filtered_projects)s of %(number_of_projects)s projects shown" +#~ msgstr "" +#~ "%(number_of_filtered_projects)s von %(number_of_projects)s Projekte " +#~ "angezeigt" + +#~ msgid "Import existing project" +#~ msgstr "Vorhandenes Projekt importieren" + +#~ msgid "Pending invitations" +#~ msgstr "Ausstehende Einladungen" + +#~ msgid "Click on one of the links to join the projects." +#~ msgstr "Klicken Sie auf einen der Links, um an den Projekten teilzunehmen." + +#~ msgid "Last changed" +#~ msgstr "Letzte Änderung" + +#~ msgid "All catalogs" +#~ msgstr "Alle Kataloge" + +#, python-format +#~ msgid "All projects on %(site)s" +#~ msgstr "Alle Projekte auf %(site)s" + #~ msgid "GitHub repository" #~ msgstr "GitHub-Repository" diff --git a/rdmo/locale/de/LC_MESSAGES/djangojs.mo b/rdmo/locale/de/LC_MESSAGES/djangojs.mo index 8e73035179..352312c054 100644 Binary files a/rdmo/locale/de/LC_MESSAGES/djangojs.mo and b/rdmo/locale/de/LC_MESSAGES/djangojs.mo differ diff --git a/rdmo/locale/de/LC_MESSAGES/djangojs.po b/rdmo/locale/de/LC_MESSAGES/djangojs.po index 5c637e9421..7f3475c433 100644 --- a/rdmo/locale/de/LC_MESSAGES/djangojs.po +++ b/rdmo/locale/de/LC_MESSAGES/djangojs.po @@ -2,8 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: RDMO\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-08 18:06+0100\n" -"PO-Revision-Date: 2023-12-04 12:54+0100\n" +"POT-Creation-Date: 2024-07-23 11:20+0200\n" +"PO-Revision-Date: 2024-07-23 11:18+0200\n" "Last-Translator: Jochen Klar <jklar@aip.de>\n" "Language-Team: RDMO <rdmo-team@listserv.dfn.de>\n" "Language: de\n" @@ -13,9 +13,33 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.4.2\n" +#: core/assets/js/components/Modal.js:16 +#: management/assets/js/components/common/Modals.js:16 +msgid "Close" +msgstr "Schließen" + +#: core/assets/js/components/Modal.js:20 +#: management/assets/js/components/common/Buttons.js:19 +msgid "Save" +msgstr "Speichern" + +#: core/assets/js/components/SearchField.js:41 +#: projects/assets/js/utils/translations.js:15 +msgid "Search" +msgstr "Suchen" + +#: core/assets/js/components/UploadDropZone.js:18 +#, javascript-format +msgid "%s has unsupported file type" +msgstr "%s hat einen nicht unterstützten Dateityp" + +#: core/assets/js/components/UploadDropZone.js:27 +msgid "Drag and drop a file here or click to select a file" +msgstr "Datei hierher ziehen oder klicken um Datei auszuwählen" + #: management/assets/js/components/common/Buttons.js:6 -#: management/assets/js/components/sidebar/ImportSidebar.js:27 -#: management/assets/js/components/sidebar/ImportSidebar.js:42 +#: management/assets/js/components/sidebar/ImportSidebar.js:34 +#: management/assets/js/components/sidebar/ImportSidebar.js:48 msgid "Back" msgstr "Zurück" @@ -35,15 +59,12 @@ msgstr "Kopieren" msgid "Copy and continue editing" msgstr "Kopieren und weiter bearbeiten" -#: management/assets/js/components/common/Buttons.js:19 -msgid "Save" -msgstr "Speichern" - #: management/assets/js/components/common/Buttons.js:19 msgid "Save and continue editing" msgstr "Speichern und weiter bearbeiten" #: management/assets/js/components/common/Buttons.js:39 +#: management/assets/js/components/import/common/ImportSelectCheckbox.js:9 msgid "New" msgstr "Neu" @@ -69,29 +90,34 @@ msgid "Upload" msgstr "Hochladen" #: management/assets/js/components/common/Links.js:77 +#: management/assets/js/components/import/ImportElement.js:48 msgid "Locked" msgstr "Gesperrt" -#: management/assets/js/components/common/Links.js:113 +#: management/assets/js/components/common/Links.js:112 +msgid "Remove your site" +msgstr "Entfernen Sie Ihre Website" + +#: management/assets/js/components/common/Links.js:112 +msgid "Add your site" +msgstr "Ihre Seite hinzufügen" + +#: management/assets/js/components/common/Links.js:131 msgid "Hide elements" msgstr "Elemente ausblenden" -#: management/assets/js/components/common/Links.js:113 +#: management/assets/js/components/common/Links.js:131 msgid "Show elements" msgstr "Elemente anzeigen" -#: management/assets/js/components/common/Links.js:129 +#: management/assets/js/components/common/Links.js:147 msgid "XML" msgstr "XML" -#: management/assets/js/components/common/Links.js:131 +#: management/assets/js/components/common/Links.js:149 msgid "XML (full)" msgstr "XML (komplett)" -#: management/assets/js/components/common/Modals.js:16 -msgid "Close" -msgstr "Schließen" - #: management/assets/js/components/edit/EditAttribute.js:37 #: management/assets/js/components/element/Attribute.js:31 msgid "This attribute is read only" @@ -99,9 +125,7 @@ msgstr "Dieses Attribut ist schreibgeschützt" #: management/assets/js/components/edit/EditAttribute.js:44 #: management/assets/js/components/element/Attribute.js:44 -#: management/assets/js/components/import/ImportAttribute.js:28 #: management/assets/js/constants/elements.js:49 -#: management/static/management/js/management.js:2 msgid "Attribute" msgstr "Attribut" @@ -119,8 +143,6 @@ msgstr "" "hinzugefügt." #: management/assets/js/components/edit/EditAttribute.js:61 -#: management/assets/js/components/edit/EditAttribute.js:68 -#: management/assets/js/components/edit/EditAttribute.js:75 #, javascript-format msgid "" "This attribute will be added to the page <code class=\"code-questions\">%s</" @@ -129,6 +151,24 @@ msgstr "" "Dieses Attribut wird der Seite <code class=\"code-questions\">%s</code> " "hinzugefügt." +#: management/assets/js/components/edit/EditAttribute.js:68 +#, javascript-format +msgid "" +"This attribute will be added to the question set <code class=\"code-questions" +"\">%s</code>." +msgstr "" +"Dieses Attribut wird dem Fragenset <code class=\"code-questions\">%s</code> " +"hinzugefügt." + +#: management/assets/js/components/edit/EditAttribute.js:75 +#, javascript-format +msgid "" +"This attribute will be added to the question <code class=\"code-questions\">" +"%s</code>." +msgstr "" +"Dieses Attribut wird der Frage <code class=\"code-questions\">%s</code> " +"hinzugefügt." + #: management/assets/js/components/edit/EditAttribute.js:82 #, javascript-format msgid "" @@ -147,15 +187,13 @@ msgid "Create new section" msgstr "Neuen Abschnitt erstellen" #: management/assets/js/components/edit/EditCatalog.js:46 -#: management/assets/js/components/element/Catalog.js:35 +#: management/assets/js/components/element/Catalog.js:37 msgid "This catalog is read only" msgstr "Dieser Katalog ist schreibgeschützt" #: management/assets/js/components/edit/EditCatalog.js:53 -#: management/assets/js/components/element/Catalog.js:51 -#: management/assets/js/components/import/ImportCatalog.js:30 +#: management/assets/js/components/element/Catalog.js:56 #: management/assets/js/constants/elements.js:44 -#: management/static/management/js/management.js:2 msgid "Catalog" msgstr "Katalog" @@ -170,9 +208,7 @@ msgstr "Diese Bedingung ist schreibgeschützt" #: management/assets/js/components/edit/EditCondition.js:48 #: management/assets/js/components/element/Condition.js:37 -#: management/assets/js/components/import/ImportCondition.js:28 #: management/assets/js/constants/elements.js:52 -#: management/static/management/js/management.js:2 msgid "Condition" msgstr "Bedingung" @@ -236,9 +272,7 @@ msgstr "Die Option ist schreibgeschützt" #: management/assets/js/components/edit/EditOption.js:45 #: management/assets/js/components/element/Option.js:37 -#: management/assets/js/components/import/ImportOption.js:28 #: management/assets/js/constants/elements.js:51 -#: management/static/management/js/management.js:2 msgid "Option" msgstr "Option" @@ -256,15 +290,13 @@ msgstr "" "hinzugefügt." #: management/assets/js/components/edit/EditOptionSet.js:45 -#: management/assets/js/components/element/OptionSet.js:31 +#: management/assets/js/components/element/OptionSet.js:35 msgid "This option set is read only" msgstr "Dieses Optionenset ist schreibgeschützt" #: management/assets/js/components/edit/EditOptionSet.js:52 -#: management/assets/js/components/element/OptionSet.js:43 -#: management/assets/js/components/import/ImportOptionSet.js:28 +#: management/assets/js/components/element/OptionSet.js:47 #: management/assets/js/constants/elements.js:50 -#: management/static/management/js/management.js:2 msgid "Option set" msgstr "Optionenset" @@ -311,31 +343,27 @@ msgstr "Fragenset: %s" #: management/assets/js/components/edit/EditPage.js:72 #: management/assets/js/components/edit/EditQuestionSet.js:72 -#, fuzzy -#| msgid "Add existing element" msgid "Add existing element" msgstr "Existierendes Element hinzufügen" #: management/assets/js/components/edit/EditPage.js:73 #: management/assets/js/components/edit/EditQuestionSet.js:73 -msgid "Create new question set" -msgstr "Neues Fragenset erstellen" +msgid "Create new question" +msgstr "Neue Frage erstellen" #: management/assets/js/components/edit/EditPage.js:74 #: management/assets/js/components/edit/EditQuestionSet.js:74 -msgid "Create new question" -msgstr "Neue Frage erstellen" +msgid "Create new question set" +msgstr "Neues Fragenset erstellen" #: management/assets/js/components/edit/EditPage.js:80 -#: management/assets/js/components/element/Page.js:42 +#: management/assets/js/components/element/Page.js:45 msgid "This page is read only" msgstr "Diese Seite ist schreibgeschützt" #: management/assets/js/components/edit/EditPage.js:87 -#: management/assets/js/components/element/Page.js:57 -#: management/assets/js/components/import/ImportPage.js:28 +#: management/assets/js/components/element/Page.js:60 #: management/assets/js/constants/elements.js:46 -#: management/static/management/js/management.js:2 msgid "Page" msgstr "Seite" @@ -352,24 +380,30 @@ msgstr "" "Diese Seite wird dem Abschnitt <code class=\"code-questions\">%s</code> " "hinzugefügt." -#: management/assets/js/components/edit/EditQuestion.js:47 -#: management/assets/js/components/element/Question.js:33 +#: management/assets/js/components/edit/EditQuestion.js:44 +msgid "Add existing optionset" +msgstr "Existierendes Optionenset hinzufügen" + +#: management/assets/js/components/edit/EditQuestion.js:45 +msgid "Create new optionset" +msgstr "Neues Optionenset erstellen" + +#: management/assets/js/components/edit/EditQuestion.js:51 +#: management/assets/js/components/element/Question.js:37 msgid "This question is read only" msgstr "Diese Frage ist schreibgschützt" -#: management/assets/js/components/edit/EditQuestion.js:54 -#: management/assets/js/components/element/Question.js:44 -#: management/assets/js/components/import/ImportQuestion.js:28 +#: management/assets/js/components/edit/EditQuestion.js:58 +#: management/assets/js/components/element/Question.js:48 #: management/assets/js/constants/elements.js:48 -#: management/static/management/js/management.js:2 msgid "Question" msgstr "Frage" -#: management/assets/js/components/edit/EditQuestion.js:56 +#: management/assets/js/components/edit/EditQuestion.js:60 msgid "Create question" msgstr "Neue Frage erstellen" -#: management/assets/js/components/edit/EditQuestion.js:63 +#: management/assets/js/components/edit/EditQuestion.js:67 #, javascript-format msgid "" "This question will be added to the page <code class=\"code-questions\">%s</" @@ -378,7 +412,7 @@ msgstr "" "Diese Frage wird der Seite <code class=\"code-questions\">%s</code> " "hinzugefügt." -#: management/assets/js/components/edit/EditQuestion.js:70 +#: management/assets/js/components/edit/EditQuestion.js:74 #, javascript-format msgid "" "This question will be added to the question set <code class=\"code-questions" @@ -388,15 +422,13 @@ msgstr "" "hinzugefügt." #: management/assets/js/components/edit/EditQuestionSet.js:80 -#: management/assets/js/components/element/QuestionSet.js:41 +#: management/assets/js/components/element/QuestionSet.js:44 msgid "This question set is read only" msgstr "Dieses Fragenset ist schreibgeschützt" #: management/assets/js/components/edit/EditQuestionSet.js:87 -#: management/assets/js/components/element/QuestionSet.js:56 -#: management/assets/js/components/import/ImportQuestionSet.js:28 +#: management/assets/js/components/element/QuestionSet.js:59 #: management/assets/js/constants/elements.js:47 -#: management/static/management/js/management.js:2 msgid "Question set" msgstr "Fragenset" @@ -437,9 +469,7 @@ msgstr "Dieser Abschnitt ist schreibgeschützt" #: management/assets/js/components/edit/EditSection.js:52 #: management/assets/js/components/element/Section.js:54 -#: management/assets/js/components/import/ImportSection.js:28 #: management/assets/js/constants/elements.js:45 -#: management/static/management/js/management.js:2 msgid "Section" msgstr "Abschnitt" @@ -457,15 +487,13 @@ msgstr "" "hinzugefügt." #: management/assets/js/components/edit/EditTask.js:44 -#: management/assets/js/components/element/Task.js:31 +#: management/assets/js/components/element/Task.js:34 msgid "This task is read only" msgstr "Diese Aufgabe ist schreibgeschützt" #: management/assets/js/components/edit/EditTask.js:51 -#: management/assets/js/components/element/Task.js:45 -#: management/assets/js/components/import/ImportTask.js:30 +#: management/assets/js/components/element/Task.js:51 #: management/assets/js/constants/elements.js:53 -#: management/static/management/js/management.js:2 msgid "Task" msgstr "Aufgabe" @@ -474,15 +502,13 @@ msgid "Create task" msgstr "Neue Aufgabe erstellen" #: management/assets/js/components/edit/EditView.js:39 -#: management/assets/js/components/element/View.js:28 +#: management/assets/js/components/element/View.js:29 msgid "This view is read only" msgstr "Diese Ansicht ist schreibgeschützt" #: management/assets/js/components/edit/EditView.js:46 -#: management/assets/js/components/element/View.js:42 -#: management/assets/js/components/import/ImportView.js:30 +#: management/assets/js/components/element/View.js:46 #: management/assets/js/constants/elements.js:54 -#: management/static/management/js/management.js:2 msgid "View" msgstr "Ansicht" @@ -508,7 +534,7 @@ msgstr "Für diese Aktion muss das Element zunächst erstellt werden." #: management/assets/js/components/edit/common/UriPrefix.js:35 #: management/assets/js/components/import/common/UriPrefix.js:21 -#: management/assets/js/components/sidebar/ImportSidebar.js:70 +#: management/assets/js/components/sidebar/ImportSidebar.js:118 msgid "Insert default URI Prefix" msgstr "Standard URI Prefix einsetzen" @@ -540,39 +566,39 @@ msgstr "Attribut sperren" msgid "Export attribute" msgstr "Attribut exportieren" -#: management/assets/js/components/element/Catalog.js:36 +#: management/assets/js/components/element/Catalog.js:38 msgid "View catalog nested" msgstr "Katalog verschachtelt anzeigen" -#: management/assets/js/components/element/Catalog.js:37 +#: management/assets/js/components/element/Catalog.js:39 msgid "Edit catalog" msgstr "Katalog bearbeiten" -#: management/assets/js/components/element/Catalog.js:38 +#: management/assets/js/components/element/Catalog.js:40 msgid "Copy catalog" msgstr "Katalog kopieren" -#: management/assets/js/components/element/Catalog.js:39 +#: management/assets/js/components/element/Catalog.js:41 msgid "Add section" msgstr "Abschnitt hinzufügen" -#: management/assets/js/components/element/Catalog.js:40 +#: management/assets/js/components/element/Catalog.js:42 msgid "Make catalog unavailable" msgstr "Katalog unverfügbar machen" -#: management/assets/js/components/element/Catalog.js:41 +#: management/assets/js/components/element/Catalog.js:43 msgid "Make catalog available" msgstr "Katalog verfügbar machen" -#: management/assets/js/components/element/Catalog.js:44 +#: management/assets/js/components/element/Catalog.js:49 msgid "Unlock catalog" msgstr "Katalog entsperren" -#: management/assets/js/components/element/Catalog.js:44 +#: management/assets/js/components/element/Catalog.js:49 msgid "Lock catalog" msgstr "Katalog sperren" -#: management/assets/js/components/element/Catalog.js:46 +#: management/assets/js/components/element/Catalog.js:51 msgid "Export catalog" msgstr "Katalog exportieren" @@ -616,109 +642,109 @@ msgstr "Option sperren" msgid "Export option" msgstr "Option exportieren" -#: management/assets/js/components/element/OptionSet.js:32 +#: management/assets/js/components/element/OptionSet.js:36 msgid "View option set nested" msgstr "Optionenset verschachtelt anzeigen" -#: management/assets/js/components/element/OptionSet.js:33 +#: management/assets/js/components/element/OptionSet.js:37 msgid "Edit option set" msgstr "Optionenset bearbeiten" -#: management/assets/js/components/element/OptionSet.js:34 +#: management/assets/js/components/element/OptionSet.js:38 msgid "Copy option set" msgstr "Optionenset kopieren" -#: management/assets/js/components/element/OptionSet.js:35 +#: management/assets/js/components/element/OptionSet.js:39 msgid "Add option" msgstr "Option hinzufügen" -#: management/assets/js/components/element/OptionSet.js:36 +#: management/assets/js/components/element/OptionSet.js:40 msgid "Unlock option set" msgstr "Optionenset entsperren" -#: management/assets/js/components/element/OptionSet.js:36 +#: management/assets/js/components/element/OptionSet.js:40 msgid "Lock option set" msgstr "Optionenset sperren" -#: management/assets/js/components/element/OptionSet.js:38 +#: management/assets/js/components/element/OptionSet.js:42 msgid "Export option set" msgstr "Optionenset exportieren" -#: management/assets/js/components/element/Page.js:43 +#: management/assets/js/components/element/Page.js:46 msgid "View page nested" msgstr "Seite verschachtelt anzeigen" -#: management/assets/js/components/element/Page.js:45 +#: management/assets/js/components/element/Page.js:48 msgid "Edit page" msgstr "Seite bearbeiten" -#: management/assets/js/components/element/Page.js:46 +#: management/assets/js/components/element/Page.js:49 msgid "Copy page" msgstr "Seite kopieren" -#: management/assets/js/components/element/Page.js:47 -#: management/assets/js/components/element/QuestionSet.js:46 +#: management/assets/js/components/element/Page.js:50 +#: management/assets/js/components/element/QuestionSet.js:49 msgid "Add question" msgstr "Frage hinzufügen" -#: management/assets/js/components/element/Page.js:47 -#: management/assets/js/components/element/QuestionSet.js:46 +#: management/assets/js/components/element/Page.js:50 +#: management/assets/js/components/element/QuestionSet.js:49 msgid "Add question set" msgstr "Fragenset hinzufügen" -#: management/assets/js/components/element/Page.js:49 +#: management/assets/js/components/element/Page.js:52 msgid "Unlock page" msgstr "Seite entsperren" -#: management/assets/js/components/element/Page.js:49 +#: management/assets/js/components/element/Page.js:52 msgid "Lock page" msgstr "Seite sperren" -#: management/assets/js/components/element/Page.js:51 +#: management/assets/js/components/element/Page.js:54 msgid "Export page" msgstr "Seite exportieren" -#: management/assets/js/components/element/Question.js:34 +#: management/assets/js/components/element/Question.js:38 msgid "Edit question" msgstr "Frage bearbeiten" -#: management/assets/js/components/element/Question.js:35 +#: management/assets/js/components/element/Question.js:39 msgid "Copy question" msgstr "Frage kopieren" -#: management/assets/js/components/element/Question.js:36 +#: management/assets/js/components/element/Question.js:40 msgid "Unlock question" msgstr "Frage entsperren" -#: management/assets/js/components/element/Question.js:36 +#: management/assets/js/components/element/Question.js:40 msgid "Lock question" msgstr "Frage sperren" -#: management/assets/js/components/element/Question.js:38 +#: management/assets/js/components/element/Question.js:42 msgid "Export question" msgstr "Frage exportieren" -#: management/assets/js/components/element/QuestionSet.js:42 +#: management/assets/js/components/element/QuestionSet.js:45 msgid "View question set nested" msgstr "Fragenset verschachtelt anzeigen" -#: management/assets/js/components/element/QuestionSet.js:44 +#: management/assets/js/components/element/QuestionSet.js:47 msgid "Edit question set" msgstr "Fragenset bearbeiten" -#: management/assets/js/components/element/QuestionSet.js:45 +#: management/assets/js/components/element/QuestionSet.js:48 msgid "Copy question set" msgstr "Fragenset kopieren" -#: management/assets/js/components/element/QuestionSet.js:48 +#: management/assets/js/components/element/QuestionSet.js:51 msgid "Unlock question set" msgstr "Fragenset entsperren" -#: management/assets/js/components/element/QuestionSet.js:48 +#: management/assets/js/components/element/QuestionSet.js:51 msgid "Lock question set" msgstr "Fragenset sperren" -#: management/assets/js/components/element/QuestionSet.js:50 +#: management/assets/js/components/element/QuestionSet.js:53 msgid "Export question set" msgstr "Fragenset exportieren" @@ -750,59 +776,59 @@ msgstr "Abschnitt sperren" msgid "Export section" msgstr "Abschnitt exportieren" -#: management/assets/js/components/element/Task.js:32 +#: management/assets/js/components/element/Task.js:35 msgid "Edit task" msgstr "Aufgabe bearbeiten" -#: management/assets/js/components/element/Task.js:33 +#: management/assets/js/components/element/Task.js:36 msgid "Copy task" msgstr "Aufgabe kopieren" -#: management/assets/js/components/element/Task.js:34 +#: management/assets/js/components/element/Task.js:37 msgid "Make task unavailable" msgstr "Aufgabe unverfügbar machen" -#: management/assets/js/components/element/Task.js:35 +#: management/assets/js/components/element/Task.js:38 msgid "Make task available" msgstr "Aufgabe verfügbar machen" -#: management/assets/js/components/element/Task.js:38 +#: management/assets/js/components/element/Task.js:44 msgid "Unlock task" msgstr "Aufgabe entsperren" -#: management/assets/js/components/element/Task.js:38 +#: management/assets/js/components/element/Task.js:44 msgid "Lock task" msgstr "Aufgabe sperren" -#: management/assets/js/components/element/Task.js:40 +#: management/assets/js/components/element/Task.js:46 msgid "Export task" msgstr "Aufgabe exportieren" -#: management/assets/js/components/element/View.js:29 +#: management/assets/js/components/element/View.js:30 msgid "Edit view" msgstr "Ansicht bearbeiten" -#: management/assets/js/components/element/View.js:30 +#: management/assets/js/components/element/View.js:31 msgid "Copy view" msgstr "Ansicht kopieren" -#: management/assets/js/components/element/View.js:31 +#: management/assets/js/components/element/View.js:32 msgid "Make view unavailable" msgstr "Ansicht unverfügbar machen" -#: management/assets/js/components/element/View.js:32 +#: management/assets/js/components/element/View.js:33 msgid "Make view available" msgstr "Ansicht verfügbar machen" -#: management/assets/js/components/element/View.js:35 +#: management/assets/js/components/element/View.js:39 msgid "Unlock view" msgstr "Ansicht entsperren" -#: management/assets/js/components/element/View.js:35 +#: management/assets/js/components/element/View.js:39 msgid "Lock view" msgstr "Ansicht sperren" -#: management/assets/js/components/element/View.js:37 +#: management/assets/js/components/element/View.js:41 msgid "Export view" msgstr "Ansicht exportieren" @@ -989,22 +1015,60 @@ msgstr "Ansichten" msgid "Filter views" msgstr "Ansichten filtern" -#: management/assets/js/components/import/ImportCatalog.js:22 -msgid "catalog" -msgstr "Katalog" - -#: management/assets/js/components/import/ImportTask.js:22 -msgid "task" -msgstr "Ansicht" - -#: management/assets/js/components/import/ImportView.js:22 -msgid "view" -msgstr "Ansicht" - +#: management/assets/js/components/import/ImportAggregatedErrorsPanel.js:31 #: management/assets/js/components/import/common/Errors.js:9 msgid "Errors" msgstr "Fehler" +#: management/assets/js/components/import/ImportAggregatedWarningsPanel.js:29 +#: management/assets/js/components/import/common/Warnings.js:8 +msgid "Warnings" +msgstr "Warnungen" + +#: management/assets/js/components/import/ImportElement.js:35 +msgid "Make unavailable" +msgstr "Unverfügbar machen" + +#: management/assets/js/components/import/ImportElement.js:36 +msgid "Make available" +msgstr "Verfügbar machen" + +#: management/assets/js/components/import/ImportSuccessElement.js:15 +#: management/assets/js/components/import/common/ImportSelectCheckbox.js:8 +msgid "Changed" +msgstr "Geändert" + +#: management/assets/js/components/import/ImportSuccessElement.js:16 +#: projects/assets/js/utils/constants.js:10 +msgid "Created" +msgstr "Erstellt" + +#: management/assets/js/components/import/ImportSuccessElement.js:31 +msgid "could not be imported" +msgstr "konnte nicht importiert werden" + +#: management/assets/js/components/import/ImportSuccessElement.js:32 +msgid "but could not be added to parent element" +msgstr "konnte aber dem übergeordneten Element nicht hinzugefügt werden" + +#: management/assets/js/components/import/common/ImportFilters.js:17 +#, javascript-format +msgid "Show only created and changed elements (%s)" +msgstr "Nur erstellte und geänderte Elemente (%s) anzeigen" + +#: management/assets/js/components/import/common/ImportFilters.js:18 +#, javascript-format +msgid "Show only new and changed elements (%s)" +msgstr "Nur neue und geänderte Elemente anzeigen (%s)" + +#: management/assets/js/components/import/common/ImportFilters.js:26 +msgid "Filter URI" +msgstr "Filter URI" + +#: management/assets/js/components/import/common/ImportFilters.js:41 +msgid "Shown" +msgstr "Angezeigt" + #: management/assets/js/components/import/common/Key.js:12 msgid "Key" msgstr "Schlüssel" @@ -1014,15 +1078,11 @@ msgid "URI path" msgstr "URI Pfad" #: management/assets/js/components/import/common/UriPrefix.js:12 -#: management/assets/js/components/sidebar/ImportSidebar.js:61 -#: management/assets/js/components/sidebar/ImportSidebar.js:65 +#: management/assets/js/components/sidebar/ImportSidebar.js:109 +#: management/assets/js/components/sidebar/ImportSidebar.js:113 msgid "URI prefix" msgstr "URI Prefix" -#: management/assets/js/components/import/common/Warnings.js:9 -msgid "Warnings" -msgstr "Warnungen" - #: management/assets/js/components/info/AttributeInfo.js:36 #, javascript-format msgid "This attribute is used for <b>%s values</b> in <b>one project</b>." @@ -1190,26 +1250,10 @@ msgid_plural "This view is used in <b>%s projects</b>." msgstr[0] "Diese Ansicht wird in <b>einem Projekt</b> verwendet." msgstr[1] "Diese Ansicht wird in <b>%s Projekten</b> verwendet." -#: management/assets/js/components/main/Import.js:26 +#: management/assets/js/components/main/Import.js:35 msgid "Import" msgstr "Import" -#: management/assets/js/components/main/Import.js:38 -msgid "created" -msgstr "erstellt" - -#: management/assets/js/components/main/Import.js:39 -msgid "updated" -msgstr "zuletzt geändert" - -#: management/assets/js/components/main/Import.js:42 -msgid "could not be imported" -msgstr "konnte nicht importiert werden" - -#: management/assets/js/components/main/Import.js:46 -msgid "but could not be added to parent element" -msgstr "konnte aber dem übergeordneten Element nicht hinzugefügt werden" - #: management/assets/js/components/modals/DeleteAttributeModal.js:7 msgid "Delete attribute" msgstr "Attribut entfernen" @@ -1328,33 +1372,256 @@ msgstr "Element ein/ausblenden:" msgid "Show elements:" msgstr "Elemente anzeigen:" -#: management/assets/js/components/sidebar/ImportSidebar.js:23 +#: management/assets/js/components/sidebar/ElementsSidebar.js:77 +msgid "Export all visible elements." +msgstr "Exportiert alle sichtbaren Elemente." + +#: management/assets/js/components/sidebar/ImportSidebar.js:30 msgid "Import successful" msgstr "Import erfolgreich" -#: management/assets/js/components/sidebar/ImportSidebar.js:35 +#: management/assets/js/components/sidebar/ImportSidebar.js:42 msgid "Import elements" msgstr "Elemente importieren" -#: management/assets/js/components/sidebar/ImportSidebar.js:39 +#: management/assets/js/components/sidebar/ImportSidebar.js:45 #, javascript-format msgid "Import one element" msgid_plural "Import %s elements" msgstr[0] "Ein Element importieren" msgstr[1] "%s Elemente importieren" -#: management/assets/js/components/sidebar/ImportSidebar.js:46 +#: management/assets/js/components/sidebar/ImportSidebar.js:52 msgid "Selection" msgstr "Auswahl" -#: management/assets/js/components/sidebar/ImportSidebar.js:51 +#: management/assets/js/components/sidebar/ImportSidebar.js:57 msgid "Select all" msgstr "Alle auswählen" -#: management/assets/js/components/sidebar/ImportSidebar.js:56 -msgid "Unselect all" +#: management/assets/js/components/sidebar/ImportSidebar.js:63 +msgid "Select changed" +msgstr "Geänderte auswählen" + +#: management/assets/js/components/sidebar/ImportSidebar.js:69 +msgid "Deselect all" msgstr "Alle abwählen" #: management/assets/js/components/sidebar/ImportSidebar.js:75 +msgid "Deselect changed" +msgstr "Geänderte abwählen" + +#: management/assets/js/components/sidebar/ImportSidebar.js:81 +msgid "Show" +msgstr "Anzeigen" + +#: management/assets/js/components/sidebar/ImportSidebar.js:85 +msgid "Show all" +msgstr "Alle einblenden" + +#: management/assets/js/components/sidebar/ImportSidebar.js:91 +msgid "Show changes" +msgstr "Änderungen anzeigen" + +#: management/assets/js/components/sidebar/ImportSidebar.js:97 +msgid "Hide all" +msgstr "Alle ausblenden" + +#: management/assets/js/components/sidebar/ImportSidebar.js:103 +msgid "Hide changes" +msgstr "Änderungen ausblenden" + +#: management/assets/js/components/sidebar/ImportSidebar.js:123 msgid "Set URI prefix for all elements" msgstr "URI-Präfix für alle Elemente festlegen" + +#: projects/assets/js/components/helper/ProjectImport.js:11 +msgid "Import directly" +msgstr "Projekt direkt importieren" + +#: projects/assets/js/components/helper/ProjectImport.js:27 +#: projects/assets/js/utils/translations.js:8 +msgid "Import from file" +msgstr "Importieren aus Datei" + +#: projects/assets/js/components/helper/Table.js:52 +msgid "Scroll to top" +msgstr "Zum Seitenanfang" + +#: projects/assets/js/components/helper/Table.js:59 +#: projects/assets/js/utils/translations.js:10 +msgid "Load more" +msgstr "Mehr laden" + +#: projects/assets/js/components/helper/Table.js:62 +#: projects/assets/js/utils/translations.js:9 +msgid "Load all" +msgstr "Alles laden" + +#: projects/assets/js/components/main/Projects.js:23 +#: projects/assets/js/utils/translations.js:13 +msgid "Pending invitations" +msgstr "Ausstehende Einladungen" + +#: projects/assets/js/components/main/Projects.js:29 +#: projects/assets/js/utils/translations.js:7 +msgid "Import project" +msgstr "Projekt importieren" + +#: projects/assets/js/components/main/Projects.js:38 +#, javascript-format +msgid "%s of %s projects are displayed" +msgstr "%s von %s Projekten werden angezeigt" + +#: projects/assets/js/components/main/Projects.js:41 +#, javascript-format +msgid "%s of %s" +msgstr "%s von %s" + +#: projects/assets/js/components/main/Projects.js:55 +#: projects/assets/js/utils/translations.js:18 +msgid "View all projects" +msgstr "Alle Projekte ansehen" + +#: projects/assets/js/components/main/Projects.js:55 +#: projects/assets/js/utils/translations.js:19 +msgid "View my projects" +msgstr "Meine Projekte ansehen" + +#: projects/assets/js/components/main/Projects.js:56 +#: projects/assets/js/utils/translations.js:11 +msgid "My projects" +msgstr "Meine Projekte" + +#: projects/assets/js/components/main/Projects.js:56 +#: projects/assets/js/utils/translations.js:3 +msgid "All projects" +msgstr "Alle Projekte" + +#: projects/assets/js/utils/constants.js:6 +msgid "Name" +msgstr "Name" + +#: projects/assets/js/utils/constants.js:7 +msgid "Role" +msgstr "Rolle" + +#: projects/assets/js/utils/constants.js:8 +#: projects/assets/js/utils/constants.js:19 +msgid "Owner" +msgstr "Besitzer" + +#: projects/assets/js/utils/constants.js:9 +msgid "Progress" +msgstr "Fortschritt" + +#: projects/assets/js/utils/constants.js:11 +msgid "Last changed" +msgstr "Letzte Änderung" + +#: projects/assets/js/utils/constants.js:16 +msgid "Author" +msgstr "Autor" + +#: projects/assets/js/utils/constants.js:17 +msgid "Guest" +msgstr "Gast" + +#: projects/assets/js/utils/constants.js:18 +msgid "Manager" +msgstr "Manager" + +#: projects/assets/js/utils/translations.js:2 +msgid "Accept" +msgstr "Akzeptieren" + +#: projects/assets/js/utils/translations.js:4 +msgid "Decline" +msgstr "Ablehnen" + +#: projects/assets/js/utils/translations.js:5 +msgid "Filter" +msgstr "Filter" + +#: projects/assets/js/utils/translations.js:6 +msgid "Hide filters" +msgstr "Filter ausblenden" + +#: projects/assets/js/utils/translations.js:12 +msgid "New project" +msgstr "Neues Projekt" + +#: projects/assets/js/utils/translations.js:14 +msgid "Reset all filters" +msgstr "Alle Filter zurücksetzen" + +#: projects/assets/js/utils/translations.js:16 +msgid "Search projects" +msgstr "Projekte durchsuchen" + +#: projects/assets/js/utils/translations.js:17 +msgid "Show filters" +msgstr "Filter anzeigen" + +#~ msgid "Change" +#~ msgstr "Ändern" + +#~ msgid "Import an RDMO XML file." +#~ msgstr "Eine RDMO-XML Datei importieren." + +#, fuzzy +#~| msgid "Filter" +#~ msgid "Filter uri" +#~ msgstr "Filter" + +#~ msgid "changed" +#~ msgstr "geändert" + +#~ msgid "created" +#~ msgstr "erstellt" + +#, fuzzy +#~| msgid "New" +#~ msgid "new" +#~ msgstr "Neu" + +#~ msgid "catalog" +#~ msgstr "Katalog" + +#~ msgid "task" +#~ msgstr "Ansicht" + +#~ msgid "view" +#~ msgstr "Ansicht" + +#~ msgid "updated" +#~ msgstr "zuletzt geändert" + +#~ msgid "Filter by catalog" +#~ msgstr "Nach Katalog filtern" + +#~ msgid "Filter by created date" +#~ msgstr "Nach Erstellungsdatum filtern" + +#~ msgid "Select start date" +#~ msgstr "Startdatum wählen" + +#~ msgid "Select end date" +#~ msgstr "Enddatum wählen" + +#~ msgid "Filter by last changed date" +#~ msgstr "Nach Änderungsdatum filtern" + +#, fuzzy +#~| msgid "Load More" +#~ msgid "Load More" +#~ msgstr "Mehr laden" + +#, fuzzy +#~| msgid "Load All" +#~ msgid "Load All" +#~ msgstr "Alles laden" + +#, fuzzy +#~ msgid "Direct import" +#~ msgstr "Projekt direkt importieren" diff --git a/rdmo/locale/es/LC_MESSAGES/djangojs.mo b/rdmo/locale/es/LC_MESSAGES/djangojs.mo index b14080add3..73fa5e5c1a 100644 Binary files a/rdmo/locale/es/LC_MESSAGES/djangojs.mo and b/rdmo/locale/es/LC_MESSAGES/djangojs.mo differ diff --git a/rdmo/locale/es/LC_MESSAGES/djangojs.po b/rdmo/locale/es/LC_MESSAGES/djangojs.po index 2428e8d185..95b12bfb2f 100644 --- a/rdmo/locale/es/LC_MESSAGES/djangojs.po +++ b/rdmo/locale/es/LC_MESSAGES/djangojs.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-09 10:45+0100\n" -"PO-Revision-Date: 2023-12-09 10:49+0100\n" +"POT-Creation-Date: 2024-04-26 16:24+0200\n" +"PO-Revision-Date: 2024-09-04 10:06+0200\n" "Last-Translator: DAVID MARTINEZ <dmartinezmu@cem.es>\n" "Language-Team: \n" "Language: es\n" @@ -16,7 +16,31 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 2.4.2\n" +"X-Generator: Poedit 3.4.2\n" + +#: core/assets/js/components/Modal.js:16 +#: management/assets/js/components/common/Modals.js:16 +msgid "Close" +msgstr "Cerrar" + +#: core/assets/js/components/Modal.js:20 +#: management/assets/js/components/common/Buttons.js:19 +msgid "Save" +msgstr "Guardar" + +#: core/assets/js/components/SearchAndFilter.js:42 +#: projects/assets/js/utils/translations.js:15 +msgid "Search" +msgstr "Búsqueda" + +#: core/assets/js/components/UploadDropZone.js:18 +#, javascript-format +msgid "%s has unsupported file type" +msgstr "%s has unsupported file type" + +#: core/assets/js/components/UploadDropZone.js:26 +msgid "Drag and drop a file here or click to select a file" +msgstr "Arrastre y suelte un archivo aquí o haga clic para seleccionarlo" #: management/assets/js/components/common/Buttons.js:6 #: management/assets/js/components/sidebar/ImportSidebar.js:27 @@ -40,10 +64,6 @@ msgstr "Copiar" msgid "Copy and continue editing" msgstr "Copiar y seguir editando" -#: management/assets/js/components/common/Buttons.js:19 -msgid "Save" -msgstr "Guardar" - #: management/assets/js/components/common/Buttons.js:19 msgid "Save and continue editing" msgstr "Guardar y seguir editando" @@ -93,10 +113,6 @@ msgstr "XML" msgid "XML (full)" msgstr "XML (completo)" -#: management/assets/js/components/common/Modals.js:16 -msgid "Close" -msgstr "Cerrar" - #: management/assets/js/components/edit/EditAttribute.js:37 #: management/assets/js/components/element/Attribute.js:31 msgid "This attribute is read only" @@ -117,8 +133,8 @@ msgstr "Crear atributo" #: management/assets/js/components/edit/EditAttribute.js:53 #, javascript-format msgid "" -"This attribute will be added to the attribute <code class=\"code-domain\">" -"%s</code>." +"This attribute will be added to the attribute <code class=\"code-" +"domain\">%s</code>." msgstr "" "Este atributo se añadirá al atributo <code class=\"code-domain\">%s</code>." @@ -136,8 +152,8 @@ msgstr "" #: management/assets/js/components/edit/EditAttribute.js:82 #, javascript-format msgid "" -"This attribute will be added to the condition <code class=\"code-conditions" -"\">%s</code>." +"This attribute will be added to the condition <code class=\"code-" +"conditions\">%s</code>." msgstr "" "Este atributo se añadirá a la condición <code class=\"code-conditions\">%s</" "code>." @@ -187,11 +203,11 @@ msgstr "Crear condición" #: management/assets/js/components/edit/EditCondition.js:57 #, javascript-format msgid "" -"This condition will be added to the option set <code class=\"code-options\">" -"%s</code>." +"This condition will be added to the option set <code class=\"code-" +"options\">%s</code>." msgstr "" -"Esta condición se añadirá al conjunto de opciones <code class=\"code-options" -"\">%s</code>." +"Esta condición se añadirá al conjunto de opciones <code class=\"code-" +"options\">%s</code>." #: management/assets/js/components/edit/EditCondition.js:64 #, javascript-format @@ -205,8 +221,8 @@ msgstr "" #: management/assets/js/components/edit/EditCondition.js:71 #, javascript-format msgid "" -"This condition will be added to the question set <code class=\"code-questions" -"\">%s</code>." +"This condition will be added to the question set <code class=\"code-" +"questions\">%s</code>." msgstr "" "Esta condición se añadirá al conjunto de preguntas <code class=\"code-" "questions\">%s</code>." @@ -214,8 +230,8 @@ msgstr "" #: management/assets/js/components/edit/EditCondition.js:78 #, javascript-format msgid "" -"This condition will be added to the question <code class=\"code-questions\">" -"%s</code>." +"This condition will be added to the question <code class=\"code-" +"questions\">%s</code>." msgstr "" "Esta condición se añadirá a la pregunta <code class=\"code-questions\">%s</" "code>." @@ -255,8 +271,8 @@ msgid "" "This option will be added to the option set <code class=\"code-options\">%s</" "code>." msgstr "" -"Esta opción se añadirá al conjunto de opciones <code class=\"code-options\">" -"%s</code>." +"Esta opción se añadirá al conjunto de opciones <code class=\"code-" +"options\">%s</code>." #: management/assets/js/components/edit/EditOptionSet.js:45 #: management/assets/js/components/element/OptionSet.js:31 @@ -278,8 +294,8 @@ msgstr "Crear conjunto de opciones" #: management/assets/js/components/edit/EditOptionSet.js:61 #, javascript-format msgid "" -"This option set will be added to the question <code class=\"code-questions\">" -"%s</code>." +"This option set will be added to the question <code class=\"code-" +"questions\">%s</code>." msgstr "" "Este conjunto de opciones se añadirá a la pregunta <code class=\"code-" "questions\">%s</code>." @@ -381,8 +397,8 @@ msgstr "" #: management/assets/js/components/edit/EditQuestion.js:70 #, javascript-format msgid "" -"This question will be added to the question set <code class=\"code-questions" -"\">%s</code>." +"This question will be added to the question set <code class=\"code-" +"questions\">%s</code>." msgstr "" "Esta pregunta se añadirá al conjunto de preguntas <code class=\"code-" "questions\">%s</code>." @@ -407,8 +423,8 @@ msgstr "Crear conjunto de preguntas" #: management/assets/js/components/edit/EditQuestionSet.js:96 #, javascript-format msgid "" -"This question set will be added to the page <code class=\"code-questions\">" -"%s</code>." +"This question set will be added to the page <code class=\"code-" +"questions\">%s</code>." msgstr "" "Este conjunto de preguntas se añadirá a la página <code class=\"code-" "questions\">%s</code>." @@ -419,8 +435,8 @@ msgid "" "This question set will be added to the question set <code class=\"code-" "questions\">%s</code>." msgstr "" -"Este conjunto de preguntas se añadirá al conjunto de preguntas <code class=" -"\"code-questions\">%s</code>." +"Este conjunto de preguntas se añadirá al conjunto de preguntas <code " +"class=\"code-questions\">%s</code>." #: management/assets/js/components/edit/EditSection.js:38 msgid "Add existing page" diff --git a/rdmo/locale/fr/LC_MESSAGES/djangojs.mo b/rdmo/locale/fr/LC_MESSAGES/djangojs.mo index cdfd59d386..800a6160c7 100644 Binary files a/rdmo/locale/fr/LC_MESSAGES/djangojs.mo and b/rdmo/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/rdmo/locale/fr/LC_MESSAGES/djangojs.po b/rdmo/locale/fr/LC_MESSAGES/djangojs.po index 9b319d6724..f56cf7ca9a 100644 --- a/rdmo/locale/fr/LC_MESSAGES/djangojs.po +++ b/rdmo/locale/fr/LC_MESSAGES/djangojs.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: RDMO\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-08-05 12:56+0200\n" -"PO-Revision-Date: 2024-02-15 12:41+0100\n" +"PO-Revision-Date: 2024-09-04 10:07+0200\n" "Last-Translator: Yonny CARDENAS, Gautier DEBAECKER, Nadia LAJILI, Gino " "MARCHETTI <dmp@cc.in2p3.fr>\n" "Language-Team: RDMO <dmp@cc.in2p3.fr>\n" @@ -12,7 +12,26 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 3.1\n" +"X-Generator: Poedit 3.4.2\n" + +#: core/assets/js/components/Modal.js:16 +#: management/assets/js/components/common/Modals.js:16 +msgid "Close" +msgstr "Fermer" + +#: core/assets/js/components/SearchAndFilter.js:42 +#: projects/assets/js/utils/translations.js:15 +msgid "Search" +msgstr "Recherche" + +#: core/assets/js/components/UploadDropZone.js:18 +#, javascript-format +msgid "%s has unsupported file type" +msgstr "%s a un type de fichier non supporté" + +#: core/assets/js/components/UploadDropZone.js:26 +msgid "Drag and drop a file here or click to select a file" +msgstr "Glisser-déposer un fichier ici ou cliquer pour sélectionner un fichier" #: management/assets/js/components/common/Buttons.js:6 #: management/assets/js/components/sidebar/ImportSidebar.js:27 @@ -89,10 +108,6 @@ msgstr "XML" msgid "XML (full)" msgstr "XML (complet)" -#: management/assets/js/components/common/Modals.js:16 -msgid "Close" -msgstr "Fermer" - #: management/assets/js/components/edit/EditAttribute.js:37 #: management/assets/js/components/element/Attribute.js:31 msgid "This attribute is read only" @@ -112,8 +127,8 @@ msgstr "Créer un attribut" #: management/assets/js/components/edit/EditAttribute.js:53 #, javascript-format msgid "" -"This attribute will be added to the attribute <code class=\"code-domain\">" -"%s</code>." +"This attribute will be added to the attribute <code class=\"code-" +"domain\">%s</code>." msgstr "" "Cet attribut sera ajouté à l'attribut <code class=\"code-domain\">%s</code>." @@ -130,8 +145,8 @@ msgstr "" #: management/assets/js/components/edit/EditAttribute.js:82 #, javascript-format msgid "" -"This attribute will be added to the condition <code class=\"code-conditions" -"\">%s</code>." +"This attribute will be added to the condition <code class=\"code-" +"conditions\">%s</code>." msgstr "" "Cet attribut sera ajouté à la condition <code class=\"code-conditions\">%s</" "code>." @@ -171,11 +186,11 @@ msgstr "Créer une condition" #: management/assets/js/components/edit/EditCondition.js:57 #, javascript-format msgid "" -"This condition will be added to the option set <code class=\"code-options\">" -"%s</code>." +"This condition will be added to the option set <code class=\"code-" +"options\">%s</code>." msgstr "" -"Cette condition sera ajoutée au jeu d'options <code class=\"code-options\">" -"%s</code>." +"Cette condition sera ajoutée au jeu d'options <code class=\"code-" +"options\">%s</code>." #: management/assets/js/components/edit/EditCondition.js:64 #, javascript-format @@ -189,8 +204,8 @@ msgstr "" #: management/assets/js/components/edit/EditCondition.js:71 #, javascript-format msgid "" -"This condition will be added to the question set <code class=\"code-questions" -"\">%s</code>." +"This condition will be added to the question set <code class=\"code-" +"questions\">%s</code>." msgstr "" "Cette condition sera ajoutée à l'ensemble de questions <code class=\"code-" "questions\">%s</code>." @@ -198,11 +213,11 @@ msgstr "" #: management/assets/js/components/edit/EditCondition.js:78 #, javascript-format msgid "" -"This condition will be added to the question <code class=\"code-questions\">" -"%s</code>." +"This condition will be added to the question <code class=\"code-" +"questions\">%s</code>." msgstr "" -"Cette condition sera ajoutée à la question <code class=\"code-questions\">" -"%s</code>." +"Cette condition sera ajoutée à la question <code class=\"code-" +"questions\">%s</code>." #: management/assets/js/components/edit/EditCondition.js:85 #, javascript-format @@ -260,11 +275,11 @@ msgstr "Créer un jeu d'options" #: management/assets/js/components/edit/EditOptionSet.js:61 #, javascript-format msgid "" -"This option set will be added to the question <code class=\"code-questions\">" -"%s</code>." +"This option set will be added to the question <code class=\"code-" +"questions\">%s</code>." msgstr "" -"Ce jeu d'options sera ajouté à la question <code class=\"code-questions\">" -"%s</code>." +"Ce jeu d'options sera ajouté à la question <code class=\"code-" +"questions\">%s</code>." #: management/assets/js/components/edit/EditPage.js:32 #: management/assets/js/components/edit/EditQuestionSet.js:32 @@ -331,8 +346,8 @@ msgstr "" #: management/assets/js/components/edit/EditQuestion.js:70 #, javascript-format msgid "" -"This question will be added to the question set <code class=\"code-questions" -"\">%s</code>." +"This question will be added to the question set <code class=\"code-" +"questions\">%s</code>." msgstr "" "Cette question sera ajoutée à l'ensemble de questions <code class=\"code-" "questions\">%s</code>." @@ -356,11 +371,11 @@ msgstr "Créer un ensemble de questions" #: management/assets/js/components/edit/EditQuestionSet.js:91 #, javascript-format msgid "" -"This question set will be added to the page <code class=\"code-questions\">" -"%s</code>." +"This question set will be added to the page <code class=\"code-" +"questions\">%s</code>." msgstr "" -"Cet ensemble de questions sera ajouté à la page <code class=\"code-questions" -"\">%s</code>." +"Cet ensemble de questions sera ajouté à la page <code class=\"code-" +"questions\">%s</code>." #: management/assets/js/components/edit/EditQuestionSet.js:98 #, javascript-format @@ -368,8 +383,8 @@ msgid "" "This question set will be added to the question set <code class=\"code-" "questions\">%s</code>." msgstr "" -"Cet ensemble de questions sera ajouté à l'ensemble de questions <code class=" -"\"code-questions\">%s</code>." +"Cet ensemble de questions sera ajouté à l'ensemble de questions <code " +"class=\"code-questions\">%s</code>." #: management/assets/js/components/edit/EditSection.js:41 #: management/assets/js/components/element/Section.js:39 diff --git a/rdmo/locale/it/LC_MESSAGES/djangojs.mo b/rdmo/locale/it/LC_MESSAGES/djangojs.mo index 4acfb7a444..2b7bbd51b8 100644 Binary files a/rdmo/locale/it/LC_MESSAGES/djangojs.mo and b/rdmo/locale/it/LC_MESSAGES/djangojs.mo differ diff --git a/rdmo/locale/it/LC_MESSAGES/djangojs.po b/rdmo/locale/it/LC_MESSAGES/djangojs.po index 66166f7f8b..9731e48322 100644 --- a/rdmo/locale/it/LC_MESSAGES/djangojs.po +++ b/rdmo/locale/it/LC_MESSAGES/djangojs.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-12-09 10:33+0100\n" +"POT-Creation-Date: 2024-04-26 16:24+0200\n" "PO-Revision-Date: \n" "Last-Translator: Dario Pilori <d.pilori@inrim.it>\n" "Language-Team: \n" @@ -11,7 +11,31 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 2.4.2\n" +"X-Generator: Poedit 3.4.2\n" + +#: core/assets/js/components/Modal.js:16 +#: management/assets/js/components/common/Modals.js:16 +msgid "Close" +msgstr "Chiudi" + +#: core/assets/js/components/Modal.js:20 +#: management/assets/js/components/common/Buttons.js:19 +msgid "Save" +msgstr "Salva" + +#: core/assets/js/components/SearchAndFilter.js:42 +#: projects/assets/js/utils/translations.js:15 +msgid "Search" +msgstr "Ricerca" + +#: core/assets/js/components/UploadDropZone.js:18 +#, javascript-format +msgid "%s has unsupported file type" +msgstr "%s ha un tipo di file non supportato" + +#: core/assets/js/components/UploadDropZone.js:26 +msgid "Drag and drop a file here or click to select a file" +msgstr "Trascinare e rilasciare un file qui o fare clic per selezionarlo" #: management/assets/js/components/common/Buttons.js:6 #: management/assets/js/components/sidebar/ImportSidebar.js:27 @@ -35,10 +59,6 @@ msgstr "Copia" msgid "Copy and continue editing" msgstr "Copiare e continuare a modificare" -#: management/assets/js/components/common/Buttons.js:19 -msgid "Save" -msgstr "Salva" - #: management/assets/js/components/common/Buttons.js:19 msgid "Save and continue editing" msgstr "Salvare e continuare a modificare" @@ -88,10 +108,6 @@ msgstr "XML" msgid "XML (full)" msgstr "XML (completo)" -#: management/assets/js/components/common/Modals.js:16 -msgid "Close" -msgstr "Chiudi" - #: management/assets/js/components/edit/EditAttribute.js:37 #: management/assets/js/components/element/Attribute.js:31 msgid "This attribute is read only" @@ -112,11 +128,11 @@ msgstr "Crea attributo" #: management/assets/js/components/edit/EditAttribute.js:53 #, javascript-format msgid "" -"This attribute will be added to the attribute <code class=\"code-domain\">" -"%s</code>." +"This attribute will be added to the attribute <code class=\"code-" +"domain\">%s</code>." msgstr "" -"Questo attributo verrà aggiunto all'attributo <code class=\"code-domain\">" -"%s</code>." +"Questo attributo verrà aggiunto all'attributo <code class=\"code-" +"domain\">%s</code>." #: management/assets/js/components/edit/EditAttribute.js:61 #: management/assets/js/components/edit/EditAttribute.js:68 @@ -126,17 +142,17 @@ msgid "" "This attribute will be added to the page <code class=\"code-questions\">%s</" "code>." msgstr "" -"Questo attributo verrà aggiunto alla pagina <code class=\"code-questions\">" -"%s</code>." +"Questo attributo verrà aggiunto alla pagina <code class=\"code-" +"questions\">%s</code>." #: management/assets/js/components/edit/EditAttribute.js:82 #, javascript-format msgid "" -"This attribute will be added to the condition <code class=\"code-conditions" -"\">%s</code>." +"This attribute will be added to the condition <code class=\"code-" +"conditions\">%s</code>." msgstr "" -"Questo attributo verrà aggiunto alla condizione <code class=\"code-conditions" -"\">%s</code>." +"Questo attributo verrà aggiunto alla condizione <code class=\"code-" +"conditions\">%s</code>." #: management/assets/js/components/edit/EditCatalog.js:39 msgid "Add existing section" @@ -183,8 +199,8 @@ msgstr "Crea condizione" #: management/assets/js/components/edit/EditCondition.js:57 #, javascript-format msgid "" -"This condition will be added to the option set <code class=\"code-options\">" -"%s</code>." +"This condition will be added to the option set <code class=\"code-" +"options\">%s</code>." msgstr "" "Questa condizione verrà aggiunta all'insieme di opzioni <code class=\"code-" "options\">%s</code>." @@ -195,14 +211,14 @@ msgid "" "This condition will be added to the page <code class=\"code-questions\">%s</" "code>." msgstr "" -"Questa condizione verrà aggiunta alla pagina <code class=\"code-questions\">" -"%s</code>." +"Questa condizione verrà aggiunta alla pagina <code class=\"code-" +"questions\">%s</code>." #: management/assets/js/components/edit/EditCondition.js:71 #, javascript-format msgid "" -"This condition will be added to the question set <code class=\"code-questions" -"\">%s</code>." +"This condition will be added to the question set <code class=\"code-" +"questions\">%s</code>." msgstr "" "Questa condizione sarà aggiunta all'insieme di domande <code class=\"code-" "questions\">%s</code>." @@ -210,11 +226,11 @@ msgstr "" #: management/assets/js/components/edit/EditCondition.js:78 #, javascript-format msgid "" -"This condition will be added to the question <code class=\"code-questions\">" -"%s</code>." +"This condition will be added to the question <code class=\"code-" +"questions\">%s</code>." msgstr "" -"Questa condizione sarà aggiunta alla domanda <code class=\"code-questions\">" -"%s</code>." +"Questa condizione sarà aggiunta alla domanda <code class=\"code-" +"questions\">%s</code>." #: management/assets/js/components/edit/EditCondition.js:85 #, javascript-format @@ -275,11 +291,11 @@ msgstr "Crea raccolta di opzioni" #: management/assets/js/components/edit/EditOptionSet.js:61 #, javascript-format msgid "" -"This option set will be added to the question <code class=\"code-questions\">" -"%s</code>." +"This option set will be added to the question <code class=\"code-" +"questions\">%s</code>." msgstr "" -"Questo set di opzioni sarà aggiunto alla domanda <code class=\"code-questions" -"\">%s</code>." +"Questo set di opzioni sarà aggiunto alla domanda <code class=\"code-" +"questions\">%s</code>." #: management/assets/js/components/edit/EditOptionSet.js:99 msgid "Add existing option" @@ -379,8 +395,8 @@ msgstr "" #: management/assets/js/components/edit/EditQuestion.js:70 #, javascript-format msgid "" -"This question will be added to the question set <code class=\"code-questions" -"\">%s</code>." +"This question will be added to the question set <code class=\"code-" +"questions\">%s</code>." msgstr "" "Questa domanda sarà aggiunta all'insieme di domande <code class=\"code-" "questions\">%s</code>." @@ -405,8 +421,8 @@ msgstr "Creare un set di domande" #: management/assets/js/components/edit/EditQuestionSet.js:96 #, javascript-format msgid "" -"This question set will be added to the page <code class=\"code-questions\">" -"%s</code>." +"This question set will be added to the page <code class=\"code-" +"questions\">%s</code>." msgstr "" "Questo insieme di domande sarà aggiunto alla pagina <code class=\"code-" "questions\">%s</code>." @@ -417,8 +433,8 @@ msgid "" "This question set will be added to the question set <code class=\"code-" "questions\">%s</code>." msgstr "" -"Questo insieme di domande sarà aggiunto all'insieme di domande <code class=" -"\"code-questions\">%s</code>." +"Questo insieme di domande sarà aggiunto all'insieme di domande <code " +"class=\"code-questions\">%s</code>." #: management/assets/js/components/edit/EditSection.js:38 msgid "Add existing page" diff --git a/rdmo/management/assets/js/actions/configActions.js b/rdmo/management/assets/js/actions/configActions.js index baf9203e5f..ec2a21fb75 100644 --- a/rdmo/management/assets/js/actions/configActions.js +++ b/rdmo/management/assets/js/actions/configActions.js @@ -47,7 +47,7 @@ export function toggleElements(element) { } } -export function toggleDescandants(element, elementType) { +export function toggleDescendants(element, elementType) { return (dispatch) => { findDescendants(element, elementType).forEach(e => dispatch(toggleElements(e))) } diff --git a/rdmo/management/assets/js/actions/elementActions.js b/rdmo/management/assets/js/actions/elementActions.js index e16db608db..a91633d940 100644 --- a/rdmo/management/assets/js/actions/elementActions.js +++ b/rdmo/management/assets/js/actions/elementActions.js @@ -235,9 +235,19 @@ export function fetchElement(elementType, elementId, elementAction=null) { QuestionsApi.fetchQuestions('index'), TasksApi.fetchTasks('index'), ]).then(([element, attributes, conditions, pages, questionsets, - questions, tasks]) => ({ + questions, tasks]) => { + if (elementAction == 'copy') { + delete element.conditions + delete element.pages + delete element.questionsets + delete element.questions + delete element.tasks + } + + return { element, attributes, conditions, pages, questionsets, questions, tasks - })) + } + }) } break @@ -251,9 +261,14 @@ export function fetchElement(elementType, elementId, elementAction=null) { ConditionsApi.fetchConditions('index'), OptionsApi.fetchOptions('index'), QuestionsApi.fetchQuestions('index') - ]).then(([element, conditions, options, questions]) => ({ - element, conditions, options, questions - })) + ]).then(([element, conditions, options, questions]) => { + if (elementAction == 'copy') { + delete element.questions + } + return { + element, conditions, options, questions + } + }) } break @@ -265,6 +280,7 @@ export function fetchElement(elementType, elementId, elementAction=null) { ]).then(([element, optionsets, conditions]) => { if (elementAction == 'copy') { delete element.optionsets + delete element.conditions } return { element, optionsets, conditions @@ -283,9 +299,18 @@ export function fetchElement(elementType, elementId, elementAction=null) { QuestionsApi.fetchQuestions('index'), TasksApi.fetchTasks('index'), ]).then(([element, attributes, optionsets, options, - pages, questionsets, questions, tasks]) => ({ - element, attributes, optionsets, options, pages, questionsets, questions, tasks - })) + pages, questionsets, questions, tasks]) => { + if (elementAction == 'copy') { + delete element.optionsets + delete element.pages + delete element.questionsets + delete element.questions + delete element.tasks + } + return { + element, attributes, optionsets, options, pages, questionsets, questions, tasks + } + }) break case 'tasks': @@ -342,14 +367,15 @@ export function fetchElementError(error) { // store element -export function storeElement(elementType, element, back) { +export function storeElement(elementType, element, elementAction = null, back = false) { return function(dispatch, getState) { + dispatch(storeElementInit(element)) let action switch (elementType) { case 'catalogs': - action = () => QuestionsApi.storeCatalog(element) + action = () => QuestionsApi.storeCatalog(element, elementAction) break case 'sections': @@ -385,11 +411,11 @@ export function storeElement(elementType, element, back) { break case 'tasks': - action = () => TasksApi.storeTask(element) + action = () => TasksApi.storeTask(element, elementAction) break case 'views': - action = () => ViewsApi.storeView(element) + action = () => ViewsApi.storeView(element, elementAction) break } @@ -398,7 +424,7 @@ export function storeElement(elementType, element, back) { dispatch(storeElementSuccess(element)) if (back) { history.back() - } else if (getState().elements.elementAction == 'create') { + } else if (['create', 'copy'].includes(getState().elements.elementAction)) { dispatch(fetchElement(getState().elements.elementType, element.id)) } }) @@ -574,7 +600,6 @@ export function deleteElement(elementType, element) { case 'catalogs': action = () => QuestionsApi.deleteCatalog(element) break - case 'sections': action = () => QuestionsApi.deleteSection(element) break @@ -653,9 +678,9 @@ export function dropElement(dragElement, dropElement, mode) { const element = {...getState().elements.element} const { dragParent, dropParent } = moveElement(element, dragElement, dropElement, mode) - dispatch(storeElement(elementTypes[dragParent.model], dragParent)) - if (!isNil(dropParent)) { - dispatch(storeElement(elementTypes[dropParent.model], dropParent)) + dispatch(storeElement(elementTypes[dragParent.model], dragParent)) + if (!isNil(dropParent)) { + dispatch(storeElement(elementTypes[dropParent.model], dropParent)) } } } diff --git a/rdmo/management/assets/js/actions/importActions.js b/rdmo/management/assets/js/actions/importActions.js index fa88c51870..b65dc32cbd 100644 --- a/rdmo/management/assets/js/actions/importActions.js +++ b/rdmo/management/assets/js/actions/importActions.js @@ -8,7 +8,7 @@ import { fetchElements, fetchElement } from './elementActions' export function uploadFile(file) { return function(dispatch) { - dispatch(uploadFileInit()) + dispatch(uploadFileInit(file)) return ManagementApi.uploadFile(file) .then(elements => dispatch(uploadFileSuccess(elements))) @@ -18,8 +18,8 @@ export function uploadFile(file) { } } -export function uploadFileInit() { - return {type: 'import/uploadFileInit'} +export function uploadFileInit(file) { + return {type: 'import/uploadFileInit', file: file} } export function uploadFileSuccess(elements) { @@ -65,6 +65,16 @@ export function updateElement(element, values) { export function selectElements(value) { return {type: 'import/selectElements', value} } +export function selectChangedElements(value) { + return {type: 'import/selectChangedElements', value} +} + +export function showElements(value) { + return {type: 'import/showElements', value} +} +export function showChangedElements(value) { + return {type: 'import/showChangedElements', value} +} export function updateUriPrefix(uriPrefix) { return {type: 'import/updateUriPrefix', uriPrefix} diff --git a/rdmo/management/assets/js/api/QuestionsApi.js b/rdmo/management/assets/js/api/QuestionsApi.js index 2a3b6f99c9..ca8895b96b 100644 --- a/rdmo/management/assets/js/api/QuestionsApi.js +++ b/rdmo/management/assets/js/api/QuestionsApi.js @@ -17,11 +17,12 @@ class QuestionsApi extends BaseApi { return this.get(url) } - static storeCatalog(catalog) { + static storeCatalog(catalog, action) { if (isNil(catalog.id)) { return this.post('/api/v1/questions/catalogs/', catalog) } else { - return this.put(`/api/v1/questions/catalogs/${catalog.id}/`, catalog) + const actionPath = isNil(action) ? '' : `${action}/` + return this.put(`/api/v1/questions/catalogs/${catalog.id}/${actionPath}`, catalog) } } diff --git a/rdmo/management/assets/js/api/TasksApi.js b/rdmo/management/assets/js/api/TasksApi.js index b6a1d0f949..513d42de8b 100644 --- a/rdmo/management/assets/js/api/TasksApi.js +++ b/rdmo/management/assets/js/api/TasksApi.js @@ -14,11 +14,12 @@ class TasksApi extends BaseApi { return this.get(`/api/v1/tasks/tasks/${id}/`) } - static storeTask(task) { + static storeTask(task, action) { if (isNil(task.id)) { return this.post('/api/v1/tasks/tasks/', task) } else { - return this.put(`/api/v1/tasks/tasks/${task.id}/`, task) + const actionPath = isNil(action) ? '' : `${action}/` + return this.put(`/api/v1/tasks/tasks/${task.id}/${actionPath}`, task) } } diff --git a/rdmo/management/assets/js/api/ViewsApi.js b/rdmo/management/assets/js/api/ViewsApi.js index 02a11b7da3..f9174cafb0 100644 --- a/rdmo/management/assets/js/api/ViewsApi.js +++ b/rdmo/management/assets/js/api/ViewsApi.js @@ -14,11 +14,12 @@ class ViewsApi extends BaseApi { return this.get(`/api/v1/views/views/${id}/`) } - static storeView(view) { + static storeView(view, action) { if (isNil(view.id)) { return this.post('/api/v1/views/views/', view) } else { - return this.put(`/api/v1/views/views/${view.id}/`, view) + const actionPath = isNil(action) ? '' : `${action}/` + return this.put(`/api/v1/views/views/${view.id}/${actionPath}`, view) } } diff --git a/rdmo/management/assets/js/components/common/Icons.js b/rdmo/management/assets/js/components/common/Icons.js index 6456be6de4..afb21d8460 100644 --- a/rdmo/management/assets/js/components/common/Icons.js +++ b/rdmo/management/assets/js/components/common/Icons.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' const ReadOnlyIcon = ({ title, show }) => { return show && ( - <i className="fa fa-ban" title={title}></i> + <i className="element-button fa fa-ban" title={title}></i> ) } diff --git a/rdmo/management/assets/js/components/common/Labels.js b/rdmo/management/assets/js/components/common/Labels.js new file mode 100644 index 0000000000..f0d3e3729e --- /dev/null +++ b/rdmo/management/assets/js/components/common/Labels.js @@ -0,0 +1,21 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const Label = ({ text, type, onClick, show = true, className = '' }) => { + const labelClass = `label label-${type} ${className}` + return show && ( + <span className={labelClass} onClick={onClick}> + {text} + </span> + ) +} + +Label.propTypes = { + text: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + onClick: PropTypes.func, + show: PropTypes.bool, + className: PropTypes.string, +} + +export default Label diff --git a/rdmo/management/assets/js/components/common/Links.js b/rdmo/management/assets/js/components/common/Links.js index 8f816dd64a..51898ad635 100644 --- a/rdmo/management/assets/js/components/common/Links.js +++ b/rdmo/management/assets/js/components/common/Links.js @@ -1,7 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' -import isEmpty from 'lodash/isEmpty' import isNil from 'lodash/isNil' import isUndefined from 'lodash/isUndefined' @@ -19,14 +18,15 @@ NestedLink.propTypes = { show: PropTypes.bool } -const EditLink = ({ href, title, onClick }) => { - return <Link href={href} className="element-link fa fa-pencil" title={title} onClick={onClick} /> +const EditLink = ({ href, title, onClick, disabled= false }) => { + return <Link href={href} className="element-link fa fa-pencil" title={title} onClick={onClick} disabled={disabled} /> } EditLink.propTypes = { href: PropTypes.string.isRequired, title: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired + onClick: PropTypes.func.isRequired, + disabled: PropTypes.bool } const CopyLink = ({ href, title, onClick }) => { @@ -103,6 +103,24 @@ LockedLink.propTypes = { disabled: PropTypes.bool } +const ToggleCurrentSiteLink = ({ hasCurrentSite, onClick, show }) => { + const className = classNames({ + 'element-btn-link fa': true, + 'fa-plus-square': !hasCurrentSite, + 'fa-minus-square': hasCurrentSite, + }) + const title = hasCurrentSite ? gettext('Remove your site'): gettext('Add your site') + + return show && <LinkButton className={className} title={title} onClick={onClick} /> +} + +ToggleCurrentSiteLink.propTypes = { + hasCurrentSite: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + show: PropTypes.bool +} + + const ShowElementsLink = ({ showElements, show, onClick }) => { const className = classNames({ 'element-btn-link fa': true, @@ -180,10 +198,10 @@ ExtendLink.propTypes = { onClick: PropTypes.func.isRequired } -const CodeLink = ({ className, uri, onClick, order }) => { +const CodeLink = ({ className, uri, href, onClick, order }) => { return ( <> - <Link onClick={onClick}> + <Link href={href} onClick={onClick}> <code className={className}>{uri}</code> </Link> {!isNil(order) ? ( @@ -196,51 +214,42 @@ const CodeLink = ({ className, uri, onClick, order }) => { CodeLink.propTypes = { className: PropTypes.string.isRequired, uri: PropTypes.string.isRequired, + href: PropTypes.string, onClick: PropTypes.func.isRequired, order: PropTypes.number } -const ErrorLink = ({ element, onClick }) => { - return ( - !isEmpty(element.errors) && - <Link className="element-link fa fa-warning text-danger" onClick={onClick} /> - ) +const ErrorLink = ({ onClick }) => { + return <Link className="element-link fa fa-warning text-danger" onClick={onClick} /> } ErrorLink.propTypes = { - element: PropTypes.object.isRequired, onClick: PropTypes.func.isRequired } - -const WarningLink = ({ element, onClick }) => { - return ( - !isEmpty(element.warnings) && - <Link className="element-link fa fa-warning text-warning" onClick={onClick} /> - ) +const WarningLink = ({ onClick }) => { + return <Link className="element-link fa fa-warning text-warning" onClick={onClick} /> } WarningLink.propTypes = { - element: PropTypes.object.isRequired, onClick: PropTypes.func.isRequired } - -const ShowLink = ({ element, onClick }) => { - const title = element.show ? gettext('Hide') : gettext('Show') +const ShowLink = ({ show = false, onClick }) => { + const title = show ? gettext('Hide') : gettext('Show') const className = classNames({ 'element-link fa': true, - 'fa-eye-slash': element.show, - 'fa-eye': !element.show + 'fa-chevron-down': !show, + 'fa-chevron-up': show }) return <Link className={className} title={title} onClick={onClick} /> } ShowLink.propTypes = { - element: PropTypes.object.isRequired, + show: PropTypes.bool, onClick: PropTypes.func.isRequired } -export { EditLink, CopyLink, AddLink, AvailableLink, LockedLink, ShowElementsLink, +export { EditLink, CopyLink, AddLink, AvailableLink, ToggleCurrentSiteLink, LockedLink, ShowElementsLink, NestedLink, ExportLink, ExtendLink, CodeLink, ErrorLink, WarningLink, ShowLink } diff --git a/rdmo/management/assets/js/components/edit/EditAttribute.js b/rdmo/management/assets/js/components/edit/EditAttribute.js index 8355caf91c..1d49213573 100644 --- a/rdmo/management/assets/js/components/edit/EditAttribute.js +++ b/rdmo/management/assets/js/components/edit/EditAttribute.js @@ -23,7 +23,7 @@ const EditAttribute = ({ config, attribute, elements, elementActions }) => { const editAttribute = (attribute) => elementActions.fetchElement('attributes', attribute) const updateAttribute = (key, value) => elementActions.updateElement(attribute, {[key]: value}) - const storeAttribute = (back) => elementActions.storeElement('attributes', attribute, back) + const storeAttribute = (back) => elementActions.storeElement('attributes', attribute, elementAction, back) const deleteAttribute = () => elementActions.deleteElement('attributes', attribute) const [showDeleteModal, openDeleteModal, closeDeleteModal] = useDeleteModal() @@ -65,14 +65,14 @@ const EditAttribute = ({ config, attribute, elements, elementActions }) => { { parent && parent.questionset && <div className="panel-body panel-border"> <p dangerouslySetInnerHTML={{ - __html:interpolate(gettext('This attribute will be added to the page <code class="code-questions">%s</code>.'), [parent.questionset.uri]) + __html:interpolate(gettext('This attribute will be added to the question set <code class="code-questions">%s</code>.'), [parent.questionset.uri]) }} /> </div> } { parent && parent.question && <div className="panel-body panel-border"> <p dangerouslySetInnerHTML={{ - __html:interpolate(gettext('This attribute will be added to the page <code class="code-questions">%s</code>.'), [parent.question.uri]) + __html:interpolate(gettext('This attribute will be added to the question <code class="code-questions">%s</code>.'), [parent.question.uri]) }} /> </div> } diff --git a/rdmo/management/assets/js/components/edit/EditCatalog.js b/rdmo/management/assets/js/components/edit/EditCatalog.js index 9d688694ac..883f431c60 100644 --- a/rdmo/management/assets/js/components/edit/EditCatalog.js +++ b/rdmo/management/assets/js/components/edit/EditCatalog.js @@ -25,7 +25,7 @@ const EditCatalog = ({ config, catalog, elements, elementActions }) => { const { elementAction, sections } = elements const updateCatalog = (key, value) => elementActions.updateElement(catalog, {[key]: value}) - const storeCatalog = (back) => elementActions.storeElement('catalogs', catalog, back) + const storeCatalog = (back) => elementActions.storeElement('catalogs', catalog, elementAction, back) const deleteCatalog = () => elementActions.deleteElement('catalogs', catalog) const editSection = (value) => elementActions.fetchElement('sections', value.section) diff --git a/rdmo/management/assets/js/components/edit/EditCondition.js b/rdmo/management/assets/js/components/edit/EditCondition.js index 8cffeb4b13..6f990441fa 100644 --- a/rdmo/management/assets/js/components/edit/EditCondition.js +++ b/rdmo/management/assets/js/components/edit/EditCondition.js @@ -22,7 +22,7 @@ const EditCondition = ({ config, condition, elements, elementActions }) => { const { elementAction, parent, attributes, options } = elements const updateCondition = (key, value) => elementActions.updateElement(condition, {[key]: value}) - const storeCondition = (back) => elementActions.storeElement('conditions', condition, back) + const storeCondition = (back) => elementActions.storeElement('conditions', condition, elementAction, back) const deleteCondition = () => elementActions.deleteElement('conditions', condition) const editAttribute = (attribute) => elementActions.fetchElement('attributes', attribute) diff --git a/rdmo/management/assets/js/components/edit/EditOption.js b/rdmo/management/assets/js/components/edit/EditOption.js index 08c865deca..23776667a0 100644 --- a/rdmo/management/assets/js/components/edit/EditOption.js +++ b/rdmo/management/assets/js/components/edit/EditOption.js @@ -24,7 +24,7 @@ const EditOption = ({ config, option, elements, elementActions }) => { const { elementAction, parent } = elements const updateOption = (key, value) => elementActions.updateElement(option, {[key]: value}) - const storeOption = (back) => elementActions.storeElement('options', option, back) + const storeOption = (back) => elementActions.storeElement('options', option, elementAction, back) const deleteOption = () => elementActions.deleteElement('options', option) const [showDeleteModal, openDeleteModal, closeDeleteModal] = useDeleteModal() diff --git a/rdmo/management/assets/js/components/edit/EditOptionSet.js b/rdmo/management/assets/js/components/edit/EditOptionSet.js index 1ac9da2128..f8d3e09265 100644 --- a/rdmo/management/assets/js/components/edit/EditOptionSet.js +++ b/rdmo/management/assets/js/components/edit/EditOptionSet.js @@ -25,7 +25,7 @@ const EditOptionSet = ({ config, optionset, elements, elementActions }) => { const { elementAction, parent, conditions, options } = elements const updateOptionSet = (key, value) => elementActions.updateElement(optionset, {[key]: value}) - const storeOptionSet = (back) => elementActions.storeElement('optionsets', optionset, back) + const storeOptionSet = (back) => elementActions.storeElement('optionsets', optionset, elementAction, back) const deleteOptionSet = () => elementActions.deleteElement('optionsets', optionset) const editOption = (value) => elementActions.fetchElement('options', value.option) diff --git a/rdmo/management/assets/js/components/edit/EditPage.js b/rdmo/management/assets/js/components/edit/EditPage.js index 707a82a2d4..6c25ab0d7f 100644 --- a/rdmo/management/assets/js/components/edit/EditPage.js +++ b/rdmo/management/assets/js/components/edit/EditPage.js @@ -45,7 +45,7 @@ const EditPage = ({ config, page, elements, elementActions }) => { elementActions.updateElement(page, { [key]: value }) } } - const storePage = (back) => elementActions.storeElement('pages', page, back) + const storePage = (back) => elementActions.storeElement('pages', page, elementAction, back) const deletePage = () => elementActions.deleteElement('pages', page) const editElement = (value) => { @@ -70,8 +70,8 @@ const EditPage = ({ config, page, elements, elementActions }) => { // for reasons unknown, the strings are not picked up by makemessages from the props const addElementText = gettext('Add existing element') - const createQuestionText = gettext('Create new question set') - const createQuestionSetText = gettext('Create new question') + const createQuestionText = gettext('Create new question') + const createQuestionSetText = gettext('Create new question set') return ( <div className="panel panel-default panel-edit"> @@ -136,6 +136,8 @@ const EditPage = ({ config, page, elements, elementActions }) => { <Tab key={index} eventKey={index} title={lang}> <Text config={config} element={page} field={`title_${lang_code }`} onChange={updatePage} /> + <Text config={config} element={page} field={`short_title_${lang_code }`} + onChange={updatePage} /> <Textarea config={config} element={page} field={`help_${lang_code }`} rows={4} onChange={updatePage} /> <Text config={config} element={page} field={`verbose_name_${lang_code }`} diff --git a/rdmo/management/assets/js/components/edit/EditQuestion.js b/rdmo/management/assets/js/components/edit/EditQuestion.js index 38159af56b..5fdf2502a0 100644 --- a/rdmo/management/assets/js/components/edit/EditQuestion.js +++ b/rdmo/management/assets/js/components/edit/EditQuestion.js @@ -24,7 +24,7 @@ const EditQuestion = ({ config, question, elements, elementActions}) => { const { elementAction, parent, attributes, optionsets, options, conditions } = elements const updateQuestion = (key, value) => elementActions.updateElement(question, {[key]: value}) - const storeQuestion = (back) => elementActions.storeElement('questions', question, back) + const storeQuestion = (back) => elementActions.storeElement('questions', question, elementAction, back) const deleteQuestion = () => elementActions.deleteElement('questions', question) const editOptionSet = (optionset) => elementActions.fetchElement('optionsets', optionset) @@ -40,6 +40,10 @@ const EditQuestion = ({ config, question, elements, elementActions}) => { const info = <QuestionInfo question={question} elements={elements} elementActions={elementActions} /> + // for reasons unknown, the strings are not picked up by makemessages from the props + const addOptionText = gettext('Add existing optionset') + const createOptionText = gettext('Create new optionset') + return ( <div className="panel panel-default panel-edit"> <div className="panel-heading"> @@ -153,7 +157,7 @@ const EditQuestion = ({ config, question, elements, elementActions}) => { </Tab> <Tab key={1} eventKey={1} title={gettext('Option sets')}> <MultiSelect config={config} element={question} field="optionsets" options={optionsets} - addText={gettext('Add existing optionset')} createText={gettext('Create new optionset')} + addText={addOptionText} createText={createOptionText} onChange={updateQuestion} onCreate={createOptionSet} onEdit={editOptionSet} /> </Tab> <Tab key={2} eventKey={2} title={gettext('Range')}> diff --git a/rdmo/management/assets/js/components/edit/EditQuestionSet.js b/rdmo/management/assets/js/components/edit/EditQuestionSet.js index e23c4fa146..8736156615 100644 --- a/rdmo/management/assets/js/components/edit/EditQuestionSet.js +++ b/rdmo/management/assets/js/components/edit/EditQuestionSet.js @@ -45,7 +45,7 @@ const EditQuestionSet = ({ config, questionset, elements, elementActions }) => { elementActions.updateElement(questionset, { [key]: value }) } } - const storeQuestionSet = (back) => elementActions.storeElement('questionsets', questionset, back) + const storeQuestionSet = (back) => elementActions.storeElement('questionsets', questionset, elementAction, back) const deleteQuestionSet = () => elementActions.deleteElement('questionsets', questionset) const editElement = (value) => { @@ -70,8 +70,8 @@ const EditQuestionSet = ({ config, questionset, elements, elementActions }) => { // for reasons unknown, the strings are not picked up by makemessages from the props const addElementText = gettext('Add existing element') - const createQuestionText = gettext('Create new question set') - const createQuestionSetText = gettext('Create new question') + const createQuestionText = gettext('Create new question') + const createQuestionSetText = gettext('Create new question set') return ( <div className="panel panel-default panel-edit"> diff --git a/rdmo/management/assets/js/components/edit/EditSection.js b/rdmo/management/assets/js/components/edit/EditSection.js index 4c55d368c0..da22871f1a 100644 --- a/rdmo/management/assets/js/components/edit/EditSection.js +++ b/rdmo/management/assets/js/components/edit/EditSection.js @@ -24,7 +24,7 @@ const EditSection = ({ config, section, elements, elementActions }) => { const { elementAction, parent, pages } = elements const updateSection = (key, value) => elementActions.updateElement(section, {[key]: value}) - const storeSection = (back) => elementActions.storeElement('sections', section, back) + const storeSection = (back) => elementActions.storeElement('sections', section, elementAction, back) const deleteSection = () => elementActions.deleteElement('sections', section) const editPage = (value) => elementActions.fetchElement('pages', value.page) @@ -93,6 +93,8 @@ const EditSection = ({ config, section, elements, elementActions }) => { <Tab key={index} eventKey={index} title={lang}> <Text config={config} element={section} field={`title_${lang_code }`} onChange={updateSection} /> + <Text config={config} element={section} field={`short_title_${lang_code }`} + onChange={updateSection} /> </Tab> )) } diff --git a/rdmo/management/assets/js/components/edit/EditTask.js b/rdmo/management/assets/js/components/edit/EditTask.js index 551e039cc6..ba66dfe603 100644 --- a/rdmo/management/assets/js/components/edit/EditTask.js +++ b/rdmo/management/assets/js/components/edit/EditTask.js @@ -25,7 +25,7 @@ const EditTask = ({ config, task, elements, elementActions}) => { const { elementAction, attributes, catalogs, conditions } = elements const updateTask = (key, value) => elementActions.updateElement(task, {[key]: value}) - const storeTask = (back) => elementActions.storeElement('tasks', task, back) + const storeTask = (back) => elementActions.storeElement('tasks', task, elementAction, back) const deleteTask = () => elementActions.deleteElement('tasks', task) const editCondition = (condition) => elementActions.fetchElement('conditions', condition) diff --git a/rdmo/management/assets/js/components/edit/EditView.js b/rdmo/management/assets/js/components/edit/EditView.js index ae5bfb2986..416d450263 100644 --- a/rdmo/management/assets/js/components/edit/EditView.js +++ b/rdmo/management/assets/js/components/edit/EditView.js @@ -25,7 +25,7 @@ const EditView = ({ config, view, elements, elementActions }) => { const { elementAction, catalogs } = elements const updateView = (key, value) => elementActions.updateElement(view, {[key]: value}) - const storeView = (back) => elementActions.storeElement('views', view, back) + const storeView = (back) => elementActions.storeElement('views', view, elementAction, back) const deleteView = () => elementActions.deleteElement('views', view) const [showDeleteModal, openDeleteModal, closeDeleteModal] = useDeleteModal() diff --git a/rdmo/management/assets/js/components/element/Attribute.js b/rdmo/management/assets/js/components/element/Attribute.js index 7c8d161d7f..8d4915a36a 100644 --- a/rdmo/management/assets/js/components/element/Attribute.js +++ b/rdmo/management/assets/js/components/element/Attribute.js @@ -16,7 +16,7 @@ const Attribute = ({ config, attribute, elementActions, display='list', indent=0 const editUrl = buildPath(config.baseUrl, 'attributes', attribute.id) const copyUrl = buildPath(config.baseUrl, 'attributes', attribute.id, 'copy') const nestedUrl = buildPath(config.baseUrl, 'attributes', attribute.id, 'nested') - const exportUrl = buildPath('/api/v1/', 'domain', 'attributes', attribute.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'domain', 'attributes', attribute.id, 'export') const fetchEdit = () => elementActions.fetchElement('attributes', attribute.id) const fetchCopy = () => elementActions.fetchElement('attributes', attribute.id, 'copy') @@ -42,7 +42,7 @@ const Attribute = ({ config, attribute, elementActions, display='list', indent=0 <div> <p> <strong>{gettext('Attribute')}{': '}</strong> - <CodeLink className="code-domain" uri={attribute.uri} onClick={() => fetchEdit()} /> + <CodeLink className="code-domain" uri={attribute.uri} href={editUrl} onClick={() => fetchEdit()} /> </p> <ElementErrors element={attribute} /> </div> diff --git a/rdmo/management/assets/js/components/element/Catalog.js b/rdmo/management/assets/js/components/element/Catalog.js index fd934ae585..13001a614b 100644 --- a/rdmo/management/assets/js/components/element/Catalog.js +++ b/rdmo/management/assets/js/components/element/Catalog.js @@ -6,7 +6,7 @@ import { filterElement } from '../../utils/filter' import { buildPath } from '../../utils/location' import { ElementErrors } from '../common/Errors' -import { EditLink, CopyLink, AddLink, AvailableLink, LockedLink, NestedLink, +import { EditLink, CopyLink, AddLink, AvailableLink, ToggleCurrentSiteLink, LockedLink, NestedLink, ExportLink, CodeLink } from '../common/Links' import { ReadOnlyIcon } from '../common/Icons' @@ -18,7 +18,7 @@ const Catalog = ({ config, catalog, elementActions, display='list', const editUrl = buildPath(config.baseUrl, 'catalogs', catalog.id) const copyUrl = buildPath(config.baseUrl, 'catalogs', catalog.id, 'copy') const nestedUrl = buildPath(config.baseUrl, 'catalogs', catalog.id, 'nested') - const exportUrl = buildPath('/api/v1/', 'questions', 'catalogs', catalog.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'questions', 'catalogs', catalog.id, 'export') const fetchEdit = () => elementActions.fetchElement('catalogs', catalog.id) const fetchCopy = () => elementActions.fetchElement('catalogs', catalog.id, 'copy') @@ -27,6 +27,8 @@ const Catalog = ({ config, catalog, elementActions, display='list', const toggleAvailable = () => elementActions.storeElement('catalogs', {...catalog, available: !catalog.available }) const toggleLocked = () => elementActions.storeElement('catalogs', {...catalog, locked: !catalog.locked }) + const toggleCurrentSite = () => elementActions.storeElement('catalogs', catalog, 'toggle-site') + const createSection = () => elementActions.createElement('sections', { catalog }) const elementNode = ( @@ -41,6 +43,9 @@ const Catalog = ({ config, catalog, elementActions, display='list', : gettext('Make catalog available')} available={catalog.available} locked={catalog.locked} onClick={toggleAvailable} disabled={catalog.read_only} /> + <ToggleCurrentSiteLink hasCurrentSite={config.settings.multisite ? catalog.sites.includes(config.currentSite.id) : true} + onClick={toggleCurrentSite} + show={config.settings.multisite}/> <LockedLink title={catalog.locked ? gettext('Unlock catalog') : gettext('Lock catalog')} locked={catalog.locked} onClick={toggleLocked} disabled={catalog.read_only} /> <ExportLink title={gettext('Export catalog')} exportUrl={exportUrl} @@ -48,11 +53,12 @@ const Catalog = ({ config, catalog, elementActions, display='list', </div> <div> <p> - <strong>{gettext('Catalog')}{': '}</strong> {catalog.title} + <strong>{gettext('Catalog')}{': '}</strong> + <span dangerouslySetInnerHTML={{ __html: catalog.title }}></span> </p> { get(config, 'display.uri.catalogs', true) && - <CodeLink className="code-questions" uri={catalog.uri} onClick={() => fetchEdit()} /> + <CodeLink className="code-questions" uri={catalog.uri} href={editUrl} onClick={() => fetchEdit()} /> } <ElementErrors element={catalog} /> </div> diff --git a/rdmo/management/assets/js/components/element/Condition.js b/rdmo/management/assets/js/components/element/Condition.js index 816f77c65d..37c1d1c82b 100644 --- a/rdmo/management/assets/js/components/element/Condition.js +++ b/rdmo/management/assets/js/components/element/Condition.js @@ -14,7 +14,7 @@ const Condition = ({ config, condition, elementActions, filter=false, filterEdit const editUrl = buildPath(config.baseUrl, 'conditions', condition.id) const copyUrl = buildPath(config.baseUrl, 'conditions', condition.id, 'copy') - const exportUrl = buildPath('/api/v1/', 'conditions', 'conditions', condition.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'conditions', 'conditions', condition.id, 'export') const fetchEdit = () => elementActions.fetchElement('conditions', condition.id) const fetchCopy = () => elementActions.fetchElement('conditions', condition.id, 'copy') @@ -35,7 +35,7 @@ const Condition = ({ config, condition, elementActions, filter=false, filterEdit <div> <p> <strong>{gettext('Condition')}{': '}</strong> - <CodeLink className="code-conditions" uri={condition.uri} onClick={() => fetchEdit()} /> + <CodeLink className="code-conditions" uri={condition.uri} href={editUrl} onClick={() => fetchEdit()} /> </p> <ElementErrors element={condition} /> </div> diff --git a/rdmo/management/assets/js/components/element/Option.js b/rdmo/management/assets/js/components/element/Option.js index 64aac49362..a3528c6739 100644 --- a/rdmo/management/assets/js/components/element/Option.js +++ b/rdmo/management/assets/js/components/element/Option.js @@ -15,7 +15,7 @@ const Option = ({ config, option, elementActions, display='list', indent=0, filt const editUrl = buildPath(config.baseUrl, 'options', option.id) const copyUrl = buildPath(config.baseUrl, 'options', option.id, 'copy') - const exportUrl = buildPath('/api/v1/', 'options', 'options', option.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'options', 'options', option.id, 'export') const fetchEdit = () => elementActions.fetchElement('options', option.id) const fetchCopy = () => elementActions.fetchElement('options', option.id, 'copy') @@ -34,11 +34,12 @@ const Option = ({ config, option, elementActions, display='list', indent=0, filt </div> <div> <p> - <strong>{gettext('Option')}{': '}</strong> {option.text} + <strong>{gettext('Option')}{': '}</strong> + <span dangerouslySetInnerHTML={{ __html: option.text }}></span> </p> { get(config, 'display.uri.options', true) && - <CodeLink className="code-options" uri={option.uri} onClick={() => fetchEdit()} /> + <CodeLink className="code-options" uri={option.uri} href={editUrl} onClick={() => fetchEdit()} /> } <ElementErrors element={option} /> </div> diff --git a/rdmo/management/assets/js/components/element/OptionSet.js b/rdmo/management/assets/js/components/element/OptionSet.js index 3628d8efa1..cbc6cfba2e 100644 --- a/rdmo/management/assets/js/components/element/OptionSet.js +++ b/rdmo/management/assets/js/components/element/OptionSet.js @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import get from 'lodash/get' import { filterElement } from '../../utils/filter' import { buildPath } from '../../utils/location' @@ -16,7 +17,9 @@ const OptionSet = ({ config, optionset, elementActions, display='list', filter=f const editUrl = buildPath(config.baseUrl, 'optionsets', optionset.id) const copyUrl = buildPath(config.baseUrl, 'optionsets', optionset.id, 'copy') const nestedUrl = buildPath(config.baseUrl, 'optionsets', optionset.id, 'nested') - const exportUrl = buildPath('/api/v1/', 'options', 'optionsets', optionset.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'options', 'optionsets', optionset.id, 'export') + + const getConditionUrl = (index) => buildPath(config.apiUrl, 'conditions', 'conditions', optionset.conditions[index]) const fetchEdit = () => elementActions.fetchElement('optionsets', optionset.id) const fetchCopy = () => elementActions.fetchElement('optionsets', optionset.id, 'copy') @@ -24,6 +27,7 @@ const OptionSet = ({ config, optionset, elementActions, display='list', filter=f const toggleLocked = () => elementActions.storeElement('optionsets', {...optionset, locked: !optionset.locked }) const createOption = () => elementActions.createElement('options', { optionset }) + const fetchCondition = (index) => elementActions.fetchElement('conditions', optionset.conditions[index]) const elementNode = ( <div className="element"> @@ -41,8 +45,20 @@ const OptionSet = ({ config, optionset, elementActions, display='list', filter=f <div> <p> <strong>{gettext('Option set')}{': '}</strong> - <CodeLink className="code-options" uri={optionset.uri} onClick={() => fetchEdit()} /> + <CodeLink className="code-options" uri={optionset.uri} href={editUrl} onClick={() => fetchEdit()} /> </p> + { + get(config, 'display.uri.conditions', true) && optionset.condition_uris.map((uri, index) => ( + <p key={index}> + <CodeLink + className="code-conditions" + uri={uri} + href={getConditionUrl(index)} + onClick={() => fetchCondition(index)} + /> + </p> + )) + } <ElementErrors element={optionset} /> </div> </div> diff --git a/rdmo/management/assets/js/components/element/Page.js b/rdmo/management/assets/js/components/element/Page.js index 0a7e086296..e54fc2b99d 100644 --- a/rdmo/management/assets/js/components/element/Page.js +++ b/rdmo/management/assets/js/components/element/Page.js @@ -22,7 +22,10 @@ const Page = ({ config, page, configActions, elementActions, display='list', ind const editUrl = buildPath(config.baseUrl, 'pages', page.id) const copyUrl = buildPath(config.baseUrl, 'pages', page.id, 'copy') const nestedUrl = buildPath(config.baseUrl, 'pages', page.id, 'nested') - const exportUrl = buildPath('/api/v1/', 'questions', 'pages', page.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'questions', 'pages', page.id, 'export') + const attributeUrl = buildPath(config.apiUrl, 'domain', 'attributes', page.attribute) + + const getConditionUrl = (index) => buildPath(config.apiUrl, 'conditions', 'conditions', page.conditions[index]) const fetchEdit = () => elementActions.fetchElement('pages', page.id) const fetchCopy = () => elementActions.fetchElement('pages', page.id, 'copy') @@ -54,22 +57,39 @@ const Page = ({ config, page, configActions, elementActions, display='list', ind </div> <div> <p> - <strong>{gettext('Page')}{': '}</strong> {page.title} + <strong>{gettext('Page')}{': '}</strong> + <span dangerouslySetInnerHTML={{ __html: page.title }}></span> </p> { get(config, 'display.uri.pages', true) && <p> - <CodeLink className="code-questions" uri={page.uri} onClick={() => fetchEdit()} order={order} /> + <CodeLink + className="code-questions" + uri={page.uri} + href={editUrl} + onClick={() => fetchEdit()} + order={order} + /> </p> } { get(config, 'display.uri.attributes', true) && page.attribute_uri && <p> - <CodeLink className="code-domain" uri={page.attribute_uri} onClick={() => fetchAttribute()} /> + <CodeLink + className="code-domain" + uri={page.attribute_uri} + href={attributeUrl} + onClick={() => fetchAttribute()} + /> </p> } { get(config, 'display.uri.conditions', true) && page.condition_uris.map((uri, index) => ( <p key={index}> - <CodeLink className="code-conditions" uri={uri} onClick={() => fetchCondition(index)} /> + <CodeLink + className="code-conditions" + uri={uri} + href={getConditionUrl(index)} + onClick={() => fetchCondition(index)} + /> </p> )) } diff --git a/rdmo/management/assets/js/components/element/Question.js b/rdmo/management/assets/js/components/element/Question.js index e9864d8cd2..8feeaab205 100644 --- a/rdmo/management/assets/js/components/element/Question.js +++ b/rdmo/management/assets/js/components/element/Question.js @@ -17,7 +17,11 @@ const Question = ({ config, question, elementActions, display='list', indent=0, const editUrl = buildPath(config.baseUrl, 'questions', question.id) const copyUrl = buildPath(config.baseUrl, 'questions', question.id, 'copy') - const exportUrl = buildPath('/api/v1/', 'questions', 'questions', question.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'questions', 'questions', question.id, 'export') + const attributeUrl = buildPath(config.apiUrl, 'domain', 'attributes', question.attribute) + + const getConditionUrl = (index) => buildPath(config.apiUrl, 'conditions', 'conditions', question.conditions[index]) + const getOptionSetUrl = (index) => buildPath(config.apiUrl, 'options', 'optionsets', question.optionsets[index]) const fetchEdit = () => elementActions.fetchElement('questions', question.id) const fetchCopy = () => elementActions.fetchElement('questions', question.id, 'copy') @@ -42,29 +46,46 @@ const Question = ({ config, question, elementActions, display='list', indent=0, <div> <p> <strong className={question.is_optional ? 'text-muted' : ''}>{gettext('Question')}{': '}</strong> - {question.text} + <span dangerouslySetInnerHTML={{ __html: question.text }}></span> </p> { get(config, 'display.uri.questions', true) && <p> - <CodeLink className="code-questions" uri={question.uri} onClick={() => fetchEdit()} order={order} /> + <CodeLink + className="code-questions" + uri={question.uri} + href={editUrl} + onClick={() => fetchEdit()} + order={order} /> </p> } { get(config, 'display.uri.attributes', true) && question.attribute_uri && <p> - <CodeLink className="code-domain" uri={question.attribute_uri} onClick={() => fetchAttribute()} /> + <CodeLink + className="code-domain" + uri={question.attribute_uri} + href={attributeUrl} + onClick={() => fetchAttribute()} /> </p> } { get(config, 'display.uri.conditions', true) && question.condition_uris.map((uri, index) => ( <p key={index}> - <CodeLink className="code-conditions" uri={uri} onClick={() => fetchCondition(index)} /> + <CodeLink + className="code-conditions" + uri={uri} + href={getConditionUrl(index)} + onClick={() => fetchCondition(index)} /> </p> )) } { get(config, 'display.uri.optionsets', true) && question.optionset_uris.map((uri, index) => ( <p key={index}> - <CodeLink className="code-options" uri={uri} onClick={() => fetchOptionSet(index)} /> + <CodeLink + className="code-options" + uri={uri} + href={getOptionSetUrl(index)} + onClick={() => fetchOptionSet(index)} /> </p> )) } diff --git a/rdmo/management/assets/js/components/element/QuestionSet.js b/rdmo/management/assets/js/components/element/QuestionSet.js index 56d3bdd701..c72759b1d1 100644 --- a/rdmo/management/assets/js/components/element/QuestionSet.js +++ b/rdmo/management/assets/js/components/element/QuestionSet.js @@ -21,7 +21,10 @@ const QuestionSet = ({ config, questionset, configActions, elementActions, displ const editUrl = buildPath(config.baseUrl, 'questionsets', questionset.id) const copyUrl = buildPath(config.baseUrl, 'questionsets', questionset.id, 'copy') const nestedUrl = buildPath(config.baseUrl, 'questionsets', questionset.id, 'nested') - const exportUrl = buildPath('/api/v1/', 'questions', 'questionsets', questionset.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'questions', 'questionsets', questionset.id, 'export') + const attributeUrl = buildPath(config.apiUrl, 'domain', 'attributes', questionset.attribute) + + const getConditionUrl = (index) => buildPath(config.apiUrl, 'conditions', 'conditions', questionset.conditions[index]) const fetchEdit = () => elementActions.fetchElement('questionsets', questionset.id) const fetchCopy = () => elementActions.fetchElement('questionsets', questionset.id, 'copy') @@ -53,22 +56,39 @@ const QuestionSet = ({ config, questionset, configActions, elementActions, displ </div> <div> <p> - <strong>{gettext('Question set')}{': '}</strong> {questionset.title} + <strong>{gettext('Question set')}{': '}</strong> + <span dangerouslySetInnerHTML={{ __html: questionset.title }}></span> </p> { get(config, 'display.uri.questionsets', true) && <p> - <CodeLink className="code-questions" uri={questionset.uri} onClick={() => fetchEdit()} order={order} /> + <CodeLink + className="code-questions" + uri={questionset.uri} + href={editUrl} + onClick={() => fetchEdit()} + order={order} + /> </p> } { get(config, 'display.uri.attributes', true) && questionset.attribute_uri &&<p> - <CodeLink className="code-domain" uri={questionset.attribute_uri} onClick={() => fetchAttribute()} /> + <CodeLink + className="code-domain" + uri={questionset.attribute_uri} + href={attributeUrl} + onClick={() => fetchAttribute()} + /> </p> } { get(config, 'display.uri.conditions', true) && questionset.condition_uris.map((uri, index) => ( <p key={index}> - <CodeLink className="code-conditions" uri={uri} onClick={() => fetchCondition(index)} /> + <CodeLink + className="code-conditions" + uri={uri} + href={getConditionUrl(index)} + onClick={() => fetchCondition(index)} + /> </p> )) } diff --git a/rdmo/management/assets/js/components/element/Section.js b/rdmo/management/assets/js/components/element/Section.js index 92e0150ad7..6983c35d2c 100644 --- a/rdmo/management/assets/js/components/element/Section.js +++ b/rdmo/management/assets/js/components/element/Section.js @@ -23,7 +23,7 @@ const Section = ({ config, section, configActions, elementActions, display='list const editUrl = buildPath(config.baseUrl, 'sections', section.id) const copyUrl = buildPath(config.baseUrl, 'sections', section.id, 'copy') const nestedUrl = buildPath(config.baseUrl, 'sections', section.id, 'nested') - const exportUrl = buildPath('/api/v1/', 'questions', 'sections', section.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'questions', 'sections', section.id, 'export') const fetchEdit = () => elementActions.fetchElement('sections', section.id) const fetchCopy = () => elementActions.fetchElement('sections', section.id, 'copy') @@ -51,11 +51,12 @@ const Section = ({ config, section, configActions, elementActions, display='list </div> <div> <p> - <strong>{gettext('Section')}{': '}</strong> {section.title} + <strong>{gettext('Section')}{': '}</strong> + <span dangerouslySetInnerHTML={{ __html: section.title }}></span> </p> { get(config, 'display.uri.sections', true) && - <CodeLink className="code-questions" uri={section.uri} onClick={() => fetchEdit()} order={order} /> + <CodeLink className="code-questions" uri={section.uri} href={editUrl} onClick={() => fetchEdit()} order={order} /> } <ElementErrors element={section} /> </div> diff --git a/rdmo/management/assets/js/components/element/Task.js b/rdmo/management/assets/js/components/element/Task.js index 12676cc89f..1e0eee4f6f 100644 --- a/rdmo/management/assets/js/components/element/Task.js +++ b/rdmo/management/assets/js/components/element/Task.js @@ -6,7 +6,7 @@ import { filterElement } from '../../utils/filter' import { buildPath } from '../../utils/location' import { ElementErrors } from '../common/Errors' -import { EditLink, CopyLink, AvailableLink, LockedLink, ExportLink, CodeLink } from '../common/Links' +import { EditLink, CopyLink, AvailableLink, LockedLink, ExportLink, CodeLink, ToggleCurrentSiteLink } from '../common/Links' import { ReadOnlyIcon } from '../common/Icons' const Task = ({ config, task, elementActions, filter=false, filterSites=false, filterEditors=false }) => { @@ -15,12 +15,15 @@ const Task = ({ config, task, elementActions, filter=false, filterSites=false, f const editUrl = buildPath(config.baseUrl, 'tasks', task.id) const copyUrl = buildPath(config.baseUrl, 'tasks', task.id, 'copy') - const exportUrl = buildPath('/api/v1/', 'tasks', 'tasks', task.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'tasks', 'tasks', task.id, 'export') + + const getConditionUrl = (index) => buildPath(config.apiUrl, 'conditions', 'conditions', task.conditions[index]) const fetchEdit = () => elementActions.fetchElement('tasks', task.id) const fetchCopy = () => elementActions.fetchElement('tasks', task.id, 'copy') const toggleAvailable = () => elementActions.storeElement('tasks', {...task, available: !task.available }) const toggleLocked = () => elementActions.storeElement('tasks', {...task, locked: !task.locked }) + const toggleCurrentSite = () => elementActions.storeElement('tasks', task, 'toggle-site') const fetchCondition = (index) => elementActions.fetchElement('conditions', task.conditions[index]) @@ -35,6 +38,9 @@ const Task = ({ config, task, elementActions, filter=false, filterSites=false, f : gettext('Make task available')} available={task.available} locked={task.locked} onClick={toggleAvailable} disabled={task.read_only} /> + <ToggleCurrentSiteLink hasCurrentSite={config.settings.multisite ? task.sites.includes(config.currentSite.id) : true} + onClick={toggleCurrentSite} + show={config.settings.multisite}/> <LockedLink title={task.locked ? gettext('Unlock task') : gettext('Lock task')} locked={task.locked} onClick={toggleLocked} disabled={task.read_only} /> <ExportLink title={gettext('Export task')} exportUrl={exportUrl} @@ -43,17 +49,22 @@ const Task = ({ config, task, elementActions, filter=false, filterSites=false, f <div> <p> <strong>{gettext('Task')}{': '}</strong> - {task.title} + <span dangerouslySetInnerHTML={{ __html: task.title }}></span> </p> { get(config, 'display.uri.tasks', true) && <p> - <CodeLink className="code-tasks" uri={task.uri} onClick={() => fetchEdit()} /> + <CodeLink className="code-tasks" uri={task.uri} href={editUrl} onClick={() => fetchEdit()} /> </p> } { get(config, 'display.uri.conditions', true) && task.condition_uris.map((uri, index) => ( <p key={index}> - <CodeLink className="code-conditions" uri={uri} onClick={() => fetchCondition(index)} /> + <CodeLink + className="code-conditions" + uri={uri} + href={getConditionUrl(index)} + onClick={() => fetchCondition(index)} + /> </p> )) } diff --git a/rdmo/management/assets/js/components/element/View.js b/rdmo/management/assets/js/components/element/View.js index 3c1655ae81..60d51dc72b 100644 --- a/rdmo/management/assets/js/components/element/View.js +++ b/rdmo/management/assets/js/components/element/View.js @@ -5,7 +5,7 @@ import { filterElement } from '../../utils/filter' import { buildPath } from '../../utils/location' import { ElementErrors } from '../common/Errors' -import { EditLink, CopyLink, AvailableLink, LockedLink, ExportLink, CodeLink } from '../common/Links' +import { EditLink, CopyLink, AvailableLink, LockedLink, ExportLink, CodeLink, ToggleCurrentSiteLink } from '../common/Links' import { ReadOnlyIcon } from '../common/Icons' const View = ({ config, view, elementActions, filter=false, filterSites=false, filterEditors=false }) => { @@ -14,12 +14,13 @@ const View = ({ config, view, elementActions, filter=false, filterSites=false, f const editUrl = buildPath(config.baseUrl, 'views', view.id) const copyUrl = buildPath(config.baseUrl, 'views', view.id, 'copy') - const exportUrl = buildPath('/api/v1/', 'views', 'views', view.id, 'export') + const exportUrl = buildPath(config.apiUrl, 'views', 'views', view.id, 'export') const fetchEdit = () => elementActions.fetchElement('views', view.id) const fetchCopy = () => elementActions.fetchElement('views', view.id, 'copy') const toggleAvailable = () => elementActions.storeElement('views', {...view, available: !view.available }) const toggleLocked = () => elementActions.storeElement('views', {...view, locked: !view.locked }) + const toggleCurrentSite = () => elementActions.storeElement('views', view, 'toggle-site') return showElement && ( <li className="list-group-item"> @@ -32,6 +33,9 @@ const View = ({ config, view, elementActions, filter=false, filterSites=false, f : gettext('Make view available')} available={view.available} locked={view.locked} onClick={toggleAvailable} disabled={view.read_only} /> + <ToggleCurrentSiteLink hasCurrentSite={config.settings.multisite ? view.sites.includes(config.currentSite.id) : true} + onClick={toggleCurrentSite} + show={config.settings.multisite}/> <LockedLink title={view.locked ? gettext('Unlock view') : gettext('Lock view')} locked={view.locked} onClick={toggleLocked} disabled={view.read_only} /> <ExportLink title={gettext('Export view')} exportUrl={exportUrl} @@ -40,7 +44,10 @@ const View = ({ config, view, elementActions, filter=false, filterSites=false, f <div> <p> <strong>{gettext('View')}{': '}</strong> - <CodeLink className="code-views" uri={view.uri} onClick={() => fetchEdit()} /> + <span dangerouslySetInnerHTML={{ __html: view.title }}></span> + </p> + <p> + <CodeLink className="code-views" uri={view.uri} href={editUrl} onClick={() => fetchEdit()} /> </p> <ElementErrors element={view} /> </div> diff --git a/rdmo/management/assets/js/components/elements/Attributes.js b/rdmo/management/assets/js/components/elements/Attributes.js index ffb83c62d9..e89b5b8d7b 100644 --- a/rdmo/management/assets/js/components/elements/Attributes.js +++ b/rdmo/management/assets/js/components/elements/Attributes.js @@ -33,7 +33,7 @@ const Attributes = ({ config, attributes, configActions, elementActions }) => { placeholder={gettext('Filter attributes')} /> </div> <div className="col-sm-4"> - <FilterUriPrefix value={get(config, 'filter.attributes.uriPrefix', '')} onChange={updateFilterUriPrefix} + <FilterUriPrefix value={get(config, 'filter.attributes.uri_prefix', '')} onChange={updateFilterUriPrefix} options={getUriPrefixes(attributes)} /> </div> { diff --git a/rdmo/management/assets/js/components/import/ImportAggregatedErrorsPanel.js b/rdmo/management/assets/js/components/import/ImportAggregatedErrorsPanel.js new file mode 100644 index 0000000000..8f7fd000e6 --- /dev/null +++ b/rdmo/management/assets/js/components/import/ImportAggregatedErrorsPanel.js @@ -0,0 +1,56 @@ +// ImportAggregatedErrorsPanel.js +import React from 'react' +import PropTypes from 'prop-types' +import { ShowLink } from '../common/Links' +import { generateErrorMessageListItems } from './common/ErrorsListGroup' +import get from 'lodash/get' + +// Function to aggregate unique errors from elements +const aggregateUniqueErrors = (elements) => { + const allErrors = elements.reduce((acc, element) => { + return acc.concat(element.errors) + }, []) + + // Filter out duplicate errors + const uniqueErrors = [...new Set(allErrors)] + + return uniqueErrors +} + +const ImportAggregatedErrorsPanel = ({ config, elements, configActions }) => { + const updateShowErrors = () => { + const currentVal = get(config, 'filter.import.errors.show', false) + configActions.updateConfig('filter.import.errors.show', !currentVal) + } + + const showErrors = get(config, 'filter.import.errors.show', false) + + // Aggregate all unique errors into a single flat array + const uniqueErrors = aggregateUniqueErrors(elements) + + const errorsHeadingText = <strong onClick={updateShowErrors}>{gettext('Errors')} ({elements.length}) :</strong> + + return ( uniqueErrors.length > 0 && + <div className="panel panel-danger panel-import-errors mt-10"> + <div className="panel-heading" onClick={updateShowErrors}> + {errorsHeadingText} + <div className="pull-right"> + <ShowLink show={showErrors} onClick={() => {}}/> + </div> + </div> + {showErrors && ( + <ul className="list-group mb-5 pb-5 pt-5 pl-5 pr-5"> + {generateErrorMessageListItems(uniqueErrors)} + </ul> + )} + </div> + ) +} + +ImportAggregatedErrorsPanel.propTypes = { + config: PropTypes.object.isRequired, + elements: PropTypes.array.isRequired, + configActions: PropTypes.object.isRequired +} + +export default ImportAggregatedErrorsPanel diff --git a/rdmo/management/assets/js/components/import/ImportAggregatedWarningsPanel.js b/rdmo/management/assets/js/components/import/ImportAggregatedWarningsPanel.js new file mode 100644 index 0000000000..12d7dc6b58 --- /dev/null +++ b/rdmo/management/assets/js/components/import/ImportAggregatedWarningsPanel.js @@ -0,0 +1,56 @@ +// ImportAggregatedWarningsPanel.js +import React from 'react' +import PropTypes from 'prop-types' +import { ShowLink } from '../common/Links' +import { generateWarningListItems } from './common/WarningsListGroup' +import get from 'lodash/get' + +// Function to aggregate warnings from elements +const aggregateWarnings = (elements) => { + return elements.reduce((acc, element) => { + Object.entries(element.warnings).forEach(([uri, messages]) => { + acc.push({ elementWarnings: { [uri]: messages }, elementModel: element.model }) + }) + return acc + }, []) +} + +const ImportAggregatedWarningsPanel = ({ config, elements, configActions }) => { + const updateShowWarnings = () => { + const currentVal = get(config, 'filter.import.warnings.show', false) + configActions.updateConfig('filter.import.warnings.show', !currentVal) + } + + const showWarnings = get(config, 'filter.import.warnings.show', false) + + // Aggregate all warnings into a single list + const aggregatedWarnings = aggregateWarnings(elements) + + const warningsHeadingText = <strong onClick={updateShowWarnings}>{gettext('Warnings')} ({elements.length}):</strong> + + return ( aggregatedWarnings.length > 0 && + <div className="panel panel-warning panel-import-warnings mt-10"> + <div className="panel-heading" onClick={updateShowWarnings}> + {warningsHeadingText} + <div className="pull-right"> + <ShowLink show={showWarnings} onClick={() => {}}/> + </div> + </div> + {showWarnings && ( + <ul className="list-group mb-5 pb-5 pt-5 pl-5 pr-5"> + {aggregatedWarnings.map(({ elementWarnings, elementModel }) => + generateWarningListItems(elementWarnings, elementModel) + )} + </ul> + )} + </div> + ) +} + +ImportAggregatedWarningsPanel.propTypes = { + config: PropTypes.object.isRequired, + elements: PropTypes.array.isRequired, + configActions: PropTypes.object.isRequired +} + +export default ImportAggregatedWarningsPanel diff --git a/rdmo/management/assets/js/components/import/ImportAttribute.js b/rdmo/management/assets/js/components/import/ImportAttribute.js deleted file mode 100644 index a360f9c14f..0000000000 --- a/rdmo/management/assets/js/components/import/ImportAttribute.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportAttribute = ({ config, attribute, importActions }) => { - const showFields = () => importActions.updateElement(attribute, {show: !attribute.show}) - const toggleImport = () => importActions.updateElement(attribute, {import: !attribute.import}) - const updateAttribute = (key, value) => importActions.updateElement(attribute, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <WarningLink element={attribute} onClick={showFields} /> - <ErrorLink element={attribute} onClick={showFields} /> - <ShowLink element={attribute} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={attribute.import} onChange={toggleImport} /> - <strong>{gettext('Attribute')}</strong> - </label> - <CodeLink className={codeClass[attribute.model]} uri={attribute.uri} onClick={showFields} /> - </div> - { - attribute.show && <> - <Form config={config} element={attribute} updateElement={updateAttribute} /> - <Fields element={attribute} /> - <Warnings element={attribute} /> - <Errors element={attribute} /> - </> - } - </li> - ) -} - -ImportAttribute.propTypes = { - config: PropTypes.object.isRequired, - attribute: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportAttribute diff --git a/rdmo/management/assets/js/components/import/ImportCatalog.js b/rdmo/management/assets/js/components/import/ImportCatalog.js deleted file mode 100644 index 2e564e3b69..0000000000 --- a/rdmo/management/assets/js/components/import/ImportCatalog.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { AvailableLink, CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportCatalog = ({ config, catalog, importActions }) => { - const showFields = () => importActions.updateElement(catalog, {show: !catalog.show}) - const toggleImport = () => importActions.updateElement(catalog, {import: !catalog.import}) - const toggleAvailable = () => importActions.updateElement(catalog, {available: !catalog.available}) - const updateCatalog = (key, value) => importActions.updateElement(catalog, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <AvailableLink element={catalog} verboseName={gettext('catalog')} onClick={toggleAvailable} /> - <WarningLink element={catalog} onClick={showFields} /> - <ErrorLink element={catalog} onClick={showFields} /> - <ShowLink element={catalog} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={catalog.import} onChange={toggleImport} /> - <strong>{gettext('Catalog')}</strong> - </label> - <CodeLink className={codeClass[catalog.model]} uri={catalog.uri} onClick={showFields} /> - </div> - { - catalog.show && <> - <Form config={config} element={catalog} updateElement={updateCatalog} /> - <Fields element={catalog} /> - <Warnings element={catalog} /> - <Errors element={catalog} /> - </> - } - </li> - ) -} - -ImportCatalog.propTypes = { - config: PropTypes.object.isRequired, - catalog: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportCatalog diff --git a/rdmo/management/assets/js/components/import/ImportCondition.js b/rdmo/management/assets/js/components/import/ImportCondition.js deleted file mode 100644 index 58598f2429..0000000000 --- a/rdmo/management/assets/js/components/import/ImportCondition.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportCondition = ({ config, condition, importActions }) => { - const showFields = () => importActions.updateElement(condition, {show: !condition.show}) - const toggleImport = () => importActions.updateElement(condition, {import: !condition.import}) - const updateCondition = (key, value) => importActions.updateElement(condition, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <WarningLink element={condition} onClick={showFields} /> - <ErrorLink element={condition} onClick={showFields} /> - <ShowLink element={condition} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={condition.import} onChange={toggleImport} /> - <strong>{gettext('Condition')}</strong> - </label> - <CodeLink className={codeClass[condition.model]} uri={condition.uri} onClick={showFields} /> - </div> - { - condition.show && <> - <Form config={config} element={condition} updateElement={updateCondition} /> - <Fields element={condition} /> - <Warnings element={condition} /> - <Errors element={condition} /> - </> - } - </li> - ) -} - -ImportCondition.propTypes = { - config: PropTypes.object.isRequired, - condition: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportCondition diff --git a/rdmo/management/assets/js/components/import/ImportElement.js b/rdmo/management/assets/js/components/import/ImportElement.js new file mode 100644 index 0000000000..64b7887d58 --- /dev/null +++ b/rdmo/management/assets/js/components/import/ImportElement.js @@ -0,0 +1,75 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import {isEmpty} from 'lodash' + +import { + WarningLink, + ErrorLink, + ShowLink, + AvailableLink, + LockedLink, +} from '../common/Links' +import ImportSelectCheckbox from './common/ImportSelectCheckbox' +import Errors from './common/Errors' + +import Warnings from './common/Warnings' +import Fields from './common/Fields' +import Form from './common/Form' + + +const ImportElement = ({ config, element, importActions }) => { + const updateShowField = () => importActions.updateElement(element, {show: !element.show}) + const toggleImport = () => importActions.updateElement(element, {import: !element.import}) + const updateElement = (key, value) => importActions.updateElement(element, {[key]: value}) + const toggleAvailable = () => importActions.updateElement(element, {available: !element.available}) + + return ( + <li className="list-group-item"> + + <div className="pull-right"> + { + (isEmpty(element.errors) && ('available' in element)) && + <AvailableLink available={element.available} + locked={element.locked} onClick={toggleAvailable} + title={element.available ? gettext('Make unavailable') + : gettext('Make available')}/> + } + { + !isEmpty(element.warnings) && + <WarningLink onClick={updateShowField} /> + } + { + !isEmpty(element.errors) && + <ErrorLink onClick={updateShowField} /> + } + { + (element.updated && element.locked) && + <LockedLink title={gettext('Locked')} + locked={element.locked} onClick={updateShowField} disabled={true} /> + + } + <ShowLink show={element.show} onClick={updateShowField} /> + </div> + + <ImportSelectCheckbox element={element} toggleImport={toggleImport} updateShowField={updateShowField} /> + + { + element.show && <> + <Form config={config} element={element} updateElement={updateElement} /> + <Fields element={element} /> + <Errors elementErrors={element.errors} /> + <Warnings elementWarnings={element.warnings} elementModel={element.model} showTitle={true} shouldShowURI={false} /> + </> + } + </li> + ) +} + +ImportElement.propTypes = { + config: PropTypes.object.isRequired, + element: PropTypes.object.isRequired, + importActions: PropTypes.object.isRequired +} + +export default ImportElement diff --git a/rdmo/management/assets/js/components/import/ImportOption.js b/rdmo/management/assets/js/components/import/ImportOption.js deleted file mode 100644 index af8f0166ac..0000000000 --- a/rdmo/management/assets/js/components/import/ImportOption.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportOption = ({ config, option, importActions }) => { - const showFields = () => importActions.updateElement(option, {show: !option.show}) - const toggleImport = () => importActions.updateElement(option, {import: !option.import}) - const updateOption = (key, value) => importActions.updateElement(option, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <WarningLink element={option} onClick={showFields} /> - <ErrorLink element={option} onClick={showFields} /> - <ShowLink element={option} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={option.import} onChange={toggleImport} /> - <strong>{gettext('Option')}</strong> - </label> - <CodeLink className={codeClass[option.model]} uri={option.uri} onClick={showFields} /> - </div> - { - option.show && <> - <Form config={config} element={option} updateElement={updateOption} /> - <Fields element={option} /> - <Warnings element={option} /> - <Errors element={option} /> - </> - } - </li> - ) -} - -ImportOption.propTypes = { - config: PropTypes.object.isRequired, - option: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportOption diff --git a/rdmo/management/assets/js/components/import/ImportOptionSet.js b/rdmo/management/assets/js/components/import/ImportOptionSet.js deleted file mode 100644 index e7798c051d..0000000000 --- a/rdmo/management/assets/js/components/import/ImportOptionSet.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportOptionSet = ({ config, optionset, importActions }) => { - const showFields = () => importActions.updateElement(optionset, {show: !optionset.show}) - const toggleImport = () => importActions.updateElement(optionset, {import: !optionset.import}) - const updateOptionSet = (key, value) => importActions.updateElement(optionset, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <WarningLink element={optionset} onClick={showFields} /> - <ErrorLink element={optionset} onClick={showFields} /> - <ShowLink element={optionset} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={optionset.import} onChange={toggleImport} /> - <strong>{gettext('Option set')}</strong> - </label> - <CodeLink className={codeClass[optionset.model]} uri={optionset.uri} onClick={showFields} /> - </div> - { - optionset.show && <> - <Form config={config} element={optionset} updateElement={updateOptionSet} /> - <Fields element={optionset} /> - <Warnings element={optionset} /> - <Errors element={optionset} /> - </> - } - </li> - ) -} - -ImportOptionSet.propTypes = { - config: PropTypes.object.isRequired, - optionset: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportOptionSet diff --git a/rdmo/management/assets/js/components/import/ImportPage.js b/rdmo/management/assets/js/components/import/ImportPage.js deleted file mode 100644 index 2fb438f904..0000000000 --- a/rdmo/management/assets/js/components/import/ImportPage.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportPage = ({ config, page, importActions }) => { - const showFields = () => importActions.updateElement(page, {show: !page.show}) - const toggleImport = () => importActions.updateElement(page, {import: !page.import}) - const updatePage = (key, value) => importActions.updateElement(page, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <WarningLink element={page} onClick={showFields} /> - <ErrorLink element={page} onClick={showFields} /> - <ShowLink element={page} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={page.import} onChange={toggleImport} /> - <strong>{gettext('Page')}</strong> - </label> - <CodeLink className={codeClass[page.model]} uri={page.uri} onClick={showFields} /> - </div> - { - page.show && <> - <Form config={config} element={page} updateElement={updatePage} /> - <Fields element={page} /> - <Warnings element={page} /> - <Errors element={page} /> - </> - } - </li> - ) -} - -ImportPage.propTypes = { - config: PropTypes.object.isRequired, - page: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportPage diff --git a/rdmo/management/assets/js/components/import/ImportQuestion.js b/rdmo/management/assets/js/components/import/ImportQuestion.js deleted file mode 100644 index 12c757ee28..0000000000 --- a/rdmo/management/assets/js/components/import/ImportQuestion.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportQuestion = ({ config, question, importActions }) => { - const showFields = () => importActions.updateElement(question, {show: !question.show}) - const toggleImport = () => importActions.updateElement(question, {import: !question.import}) - const updateQuestion = (key, value) => importActions.updateElement(question, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <WarningLink element={question} onClick={showFields} /> - <ErrorLink element={question} onClick={showFields} /> - <ShowLink element={question} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={question.import} onChange={toggleImport} /> - <strong>{gettext('Question')}</strong> - </label> - <CodeLink className={codeClass[question.model]} uri={question.uri} onClick={showFields} /> - </div> - { - question.show && <> - <Form config={config} element={question} updateElement={updateQuestion} /> - <Fields element={question} /> - <Warnings element={question} /> - <Errors element={question} /> - </> - } - </li> - ) -} - -ImportQuestion.propTypes = { - config: PropTypes.object.isRequired, - question: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportQuestion diff --git a/rdmo/management/assets/js/components/import/ImportQuestionSet.js b/rdmo/management/assets/js/components/import/ImportQuestionSet.js deleted file mode 100644 index 8163015cbe..0000000000 --- a/rdmo/management/assets/js/components/import/ImportQuestionSet.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportQuestionSet = ({ config, questionset, importActions }) => { - const showFields = () => importActions.updateElement(questionset, {show: !questionset.show}) - const toggleImport = () => importActions.updateElement(questionset, {import: !questionset.import}) - const updateQuestionSet = (key, value) => importActions.updateElement(questionset, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <WarningLink element={questionset} onClick={showFields} /> - <ErrorLink element={questionset} onClick={showFields} /> - <ShowLink element={questionset} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={questionset.import} onChange={toggleImport} /> - <strong>{gettext('Question set')}</strong> - </label> - <CodeLink className={codeClass[questionset.model]} uri={questionset.uri} onClick={showFields} /> - </div> - { - questionset.show && <> - <Form config={config} element={questionset} updateElement={updateQuestionSet} /> - <Fields element={questionset} /> - <Warnings element={questionset} /> - <Errors element={questionset} /> - </> - } - </li> - ) -} - -ImportQuestionSet.propTypes = { - config: PropTypes.object.isRequired, - questionset: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportQuestionSet diff --git a/rdmo/management/assets/js/components/import/ImportSection.js b/rdmo/management/assets/js/components/import/ImportSection.js deleted file mode 100644 index 92203a8771..0000000000 --- a/rdmo/management/assets/js/components/import/ImportSection.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportSection = ({ config, section, importActions }) => { - const showFields = () => importActions.updateElement(section, {show: !section.show}) - const toggleImport = () => importActions.updateElement(section, {import: !section.import}) - const updateSection = (key, value) => importActions.updateElement(section, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <WarningLink element={section} onClick={showFields} /> - <ErrorLink element={section} onClick={showFields} /> - <ShowLink element={section} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={section.import} onChange={toggleImport} /> - <strong>{gettext('Section')}</strong> - </label> - <CodeLink className={codeClass[section.model]} uri={section.uri} onClick={showFields} /> - </div> - { - section.show && <> - <Form config={config} element={section} updateElement={updateSection} /> - <Fields element={section} /> - <Warnings element={section} /> - <Errors element={section} /> - </> - } - </li> - ) -} - -ImportSection.propTypes = { - config: PropTypes.object.isRequired, - section: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportSection diff --git a/rdmo/management/assets/js/components/import/ImportSuccessElement.js b/rdmo/management/assets/js/components/import/ImportSuccessElement.js new file mode 100644 index 0000000000..892f78f80c --- /dev/null +++ b/rdmo/management/assets/js/components/import/ImportSuccessElement.js @@ -0,0 +1,50 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { isEmpty } from 'lodash' + +import { codeClass, verboseNames } from '../../constants/elements' +import Warnings from './common/Warnings' +import Errors from './common/Errors' +import {ChangedLabel, CreatedLabel} from './common/ImportLabels' + + + +const ImportSuccessElement = ({ element, importActions }) => { + + const updateShowField = () => importActions.updateElement(element, { show: !element.show }) + const changedLabelText = gettext('Changed') + const createdLabelText = gettext('Created') + + return ( + <li className="list-group-item"> + <div className="mb-5"> + <strong>{verboseNames[element.model]}{' '}</strong> + <code className={codeClass[element.model]}>{element.uri}</code> + + <ChangedLabel text={changedLabelText} onClick={updateShowField} show={(element.changed && element.updated)} /> + + <CreatedLabel text={createdLabelText} onClick={updateShowField} show={element.created} /> + + { + !isEmpty(element.errors) && ( + <span className="text-danger"> + {' '}{gettext('could not be imported')} + {(element.created || element.updated) && `, ${gettext('but could not be added to parent element')}`} + {'.'} + </span> + ) + } + + </div> + <Errors elementErrors={element.errors} /> + <Warnings elementWarnings={element.warnings} elementModel={element.model} showTitle={true} shouldShowURI={false} /> + </li> + ) +} + +ImportSuccessElement.propTypes = { + element: PropTypes.object.isRequired, + importActions: PropTypes.object.isRequired +} + +export default ImportSuccessElement diff --git a/rdmo/management/assets/js/components/import/ImportTask.js b/rdmo/management/assets/js/components/import/ImportTask.js deleted file mode 100644 index 72b2b73158..0000000000 --- a/rdmo/management/assets/js/components/import/ImportTask.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { AvailableLink, CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportTask = ({ config, task, importActions }) => { - const showFields = () => importActions.updateElement(task, {show: !task.show}) - const toggleImport = () => importActions.updateElement(task, {import: !task.import}) - const toggleAvailable = () => importActions.updateElement(task, {available: !task.available}) - const updateTask = (key, value) => importActions.updateElement(task, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <AvailableLink element={task} verboseName={gettext('task')} onClick={toggleAvailable} /> - <WarningLink element={task} onClick={showFields} /> - <ErrorLink element={task} onClick={showFields} /> - <ShowLink element={task} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={task.import} onChange={toggleImport} /> - <strong>{gettext('Task')}</strong> - </label> - <CodeLink className={codeClass[task.model]} uri={task.uri} onClick={showFields} /> - </div> - { - task.show && <> - <Form config={config} element={task} updateElement={updateTask} /> - <Fields element={task} /> - <Warnings element={task} /> - <Errors element={task} /> - </> - } - </li> - ) -} - -ImportTask.propTypes = { - config: PropTypes.object.isRequired, - task: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportTask diff --git a/rdmo/management/assets/js/components/import/ImportView.js b/rdmo/management/assets/js/components/import/ImportView.js deleted file mode 100644 index e48196efb5..0000000000 --- a/rdmo/management/assets/js/components/import/ImportView.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { AvailableLink, CodeLink, WarningLink, ErrorLink, ShowLink } from '../common/Links' - -import Errors from './common/Errors' -import Fields from './common/Fields' -import Form from './common/Form' -import Warnings from './common/Warnings' - -import { codeClass } from '../../constants/elements' - -const ImportView = ({ config, view, importActions }) => { - const showFields = () => importActions.updateElement(view, {show: !view.show}) - const toggleImport = () => importActions.updateElement(view, {import: !view.import}) - const toggleAvailable = () => importActions.updateElement(view, {available: !view.available}) - const updateView = (key, value) => importActions.updateElement(view, {[key]: value}) - - return ( - <li className="list-group-item"> - <div className="pull-right"> - <AvailableLink element={view} verboseName={gettext('view')} onClick={toggleAvailable} /> - <WarningLink element={view} onClick={showFields} /> - <ErrorLink element={view} onClick={showFields} /> - <ShowLink element={view} onClick={showFields} /> - </div> - <div className="checkbox"> - <label className="mr-5"> - <input type="checkbox" checked={view.import} onChange={toggleImport} /> - <strong>{gettext('View')}</strong> - </label> - <CodeLink className={codeClass[view.model]} uri={view.uri} onClick={showFields} /> - </div> - { - view.show && <> - <Form config={config} element={view} updateElement={updateView} /> - <Fields element={view} /> - <Warnings element={view} /> - <Errors element={view} /> - </> - } - </li> - ) -} - -ImportView.propTypes = { - config: PropTypes.object.isRequired, - view: PropTypes.object.isRequired, - importActions: PropTypes.object.isRequired -} - -export default ImportView diff --git a/rdmo/management/assets/js/components/import/common/Errors.js b/rdmo/management/assets/js/components/import/common/Errors.js index 47fcd60d50..35af668236 100644 --- a/rdmo/management/assets/js/components/import/common/Errors.js +++ b/rdmo/management/assets/js/components/import/common/Errors.js @@ -1,25 +1,23 @@ +// Errors.js import React from 'react' import PropTypes from 'prop-types' -import isEmpty from 'lodash/isEmpty' -import uniqueId from 'lodash/uniqueId' +import ErrorsListGroup from './ErrorsListGroup' +import isUndefined from 'lodash/isUndefined' -const Errors = ({ element }) => { - return !isEmpty(element.errors) && <div className="row text-danger mt-10"> - <div className="col-sm-3 text-right"> - {gettext('Errors')} - </div> - <div className="col-sm-9"> - <ul className="list-unstyled"> - { - element.errors.map(message => <li key={uniqueId('error-')}>{message}</li>) - } - </ul> +const Errors = ({ elementErrors }) => { + const show = !isUndefined(elementErrors) && elementErrors.length > 0 + const errorsHeadingText = <strong>{gettext('Errors')}</strong> + + return show && ( + <div className="panel panel-danger mt-10 mb-0"> + <div className="panel-heading">{errorsHeadingText}</div> + <ErrorsListGroup elementErrors={elementErrors} /> </div> - </div> + ) } Errors.propTypes = { - element: PropTypes.object.isRequired + elementErrors: PropTypes.array.isRequired, } export default Errors diff --git a/rdmo/management/assets/js/components/import/common/ErrorsListGroup.js b/rdmo/management/assets/js/components/import/common/ErrorsListGroup.js new file mode 100644 index 0000000000..c6f209938b --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/ErrorsListGroup.js @@ -0,0 +1,31 @@ +// ErrorsListGroup.js +import React from 'react' +import PropTypes from 'prop-types' +import uniqueId from 'lodash/uniqueId' + +// Helper function to generate error messages +export const generateErrorMessageListItems = (messages) => + messages.map(message => ( + <li className="list-group-item text-danger" key={uniqueId('error-message')}> + <div className="text-danger"> + {message} + </div> + </li> + )) + +const ErrorsListGroup = ({ elementErrors }) => { + // Filter out duplicate elementErrors + const uniqueErrors = [...new Set(elementErrors)] + + return ( + <ul className="list-group"> + {generateErrorMessageListItems(uniqueErrors)} + </ul> + ) +} + +ErrorsListGroup.propTypes = { + elementErrors: PropTypes.array.isRequired +} + +export default ErrorsListGroup diff --git a/rdmo/management/assets/js/components/import/common/FieldRow.js b/rdmo/management/assets/js/components/import/common/FieldRow.js new file mode 100644 index 0000000000..33a9229763 --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/FieldRow.js @@ -0,0 +1,32 @@ +import React from 'react' +import PropTypes from 'prop-types' +import uniqueId from 'lodash/uniqueId' +import FieldRowValue from './FieldRowValue' +import FieldRowDiffs from './FieldRowDiffs' + +const FieldRow = ({ element, keyName, value }) => { + + return ( + <div> + <div className="row" key={uniqueId()}> + <div className="col-sm-3 mb-5 mt-5"> + <code className="code-import">{keyName}</code> + </div> + </div> + <div className="row" key={uniqueId()}> + <FieldRowValue value={value} /> + {element.updated && element.changed && keyName in element.updated_and_changed && ( + <FieldRowDiffs element={element} field={keyName} /> + )} + </div> + </div> + ) +} + +FieldRow.propTypes = { + element: PropTypes.object.isRequired, + keyName: PropTypes.string.isRequired, + value: PropTypes.any.isRequired, +} + +export default FieldRow diff --git a/rdmo/management/assets/js/components/import/common/FieldRowDiffs.js b/rdmo/management/assets/js/components/import/common/FieldRowDiffs.js new file mode 100644 index 0000000000..86d2e97230 --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/FieldRowDiffs.js @@ -0,0 +1,66 @@ +import React from 'react' +import PropTypes from 'prop-types' +import isEmpty from 'lodash/isEmpty' +import ReactDiffViewer from 'react-diff-viewer-continued' + +import Warnings from './Warnings' +import Errors from './Errors' + +const FieldRowDiffs = ({ element, field }) => { + if (isEmpty(element.updated_and_changed[field])) { + return null + } + const fieldDiffData = element.updated_and_changed[field] + const newVal = fieldDiffData.newValue.toString() ?? '' + const oldVal = fieldDiffData.oldValue.toString() ?? '' + const changed = fieldDiffData.changed ?? false + const splitView = false + const hideLineNumbers = true + const warnings = fieldDiffData.warnings ?? {} + const errors = fieldDiffData.errors ?? [] + + const newStyles = { + variables: { + light: { + diffViewerBackground: '#fff', + changedBackground: '#fff', + gutterBackground: '#fff', + }, + }, + contentText: { + backgroundColor: '#fff !important', + }, + } + + return (changed && + <div className="field-diff col-sm-12 mt-10 mb-10"> + <ReactDiffViewer + styles={newStyles} + oldValue={oldVal} + newValue={newVal} + splitView={splitView} + hideLineNumbers={hideLineNumbers} + // leftTitle={leftTitle} + // rightTitle={rightTitle} + > + </ReactDiffViewer> + { + !isEmpty(warnings) && <> + <Warnings elementWarnings={fieldDiffData.warnings} elementModel={element.model} showTitle={true} shouldShowURI={false}/> + </> + } + { + !isEmpty(errors) && <> + <Errors element={fieldDiffData} /> + </> + } + </div> + ) +} + +FieldRowDiffs.propTypes = { + element: PropTypes.object.isRequired, + field: PropTypes.string.isRequired, +} + +export default FieldRowDiffs diff --git a/rdmo/management/assets/js/components/import/common/FieldRowValue.js b/rdmo/management/assets/js/components/import/common/FieldRowValue.js new file mode 100644 index 0000000000..7d633fd153 --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/FieldRowValue.js @@ -0,0 +1,41 @@ +import React from 'react' +import PropTypes from 'prop-types' +import uniqueId from 'lodash/uniqueId' +import isString from 'lodash/isString' +import isPlainObject from 'lodash/isPlainObject' +import isUndefined from 'lodash/isUndefined' +import truncate from 'lodash/truncate' +import {codeClass} from '../../../constants/elements' + + +const FieldRowValue = ({ value }) => { + return ( + <div className="col-sm-12"> + { + Array.isArray(value) && ( + <ul className="list-unstyled mb-0"> + {value.map((el) => ( + <li key={uniqueId()}> + <code className={codeClass[el.model]}>{el.uri}</code> + </li> + ))} + </ul> + ) + } + { + isPlainObject(value) && !isUndefined(value.uri) && + <code className={codeClass[value.model || 'domain.attribute']}>{value.uri}</code> + } + { + isString(value) && + <span>{truncate(value, { length: 512 })}</span> + } + </div> + ) +} + +FieldRowValue.propTypes = { + value: PropTypes.any.isRequired, +} + +export default FieldRowValue diff --git a/rdmo/management/assets/js/components/import/common/Fields.js b/rdmo/management/assets/js/components/import/common/Fields.js index da483eb9cb..f49217bebd 100644 --- a/rdmo/management/assets/js/components/import/common/Fields.js +++ b/rdmo/management/assets/js/components/import/common/Fields.js @@ -1,66 +1,61 @@ import React from 'react' import PropTypes from 'prop-types' -import isNil from 'lodash/isNil' -import isString from 'lodash/isString' -import isUndefined from 'lodash/isUndefined' -import truncate from 'lodash/truncate' import uniqueId from 'lodash/uniqueId' - -import { codeClass } from '../../../constants/elements' +import FieldRow from './FieldRow' +import isString from 'lodash/isString' const excludeKeys = [ - 'created', - 'errors', 'import', 'key', 'model', 'show', 'type', - 'updated', 'uri', 'uri_path', 'uri_prefix', 'valid', - 'warnings' + 'created', + 'updated', + 'errors', + 'warnings', + 'updated_and_changed', + 'changed', + 'changedFields', + 'locked' ] + +export const serializeValue = (value) => { + if (value === null) return '' + if (value === true) return 'true' + if (value === false) return 'false' + if (Array.isArray(value)) return value + if (isString(value)) return value + if (typeof value === 'number') return value.toString() + return value +} + const Fields = ({ element }) => { + return ( <div className="mt-10"> - { - Object.entries(element).sort().map(([key, value]) => { - if (!isNil(value) && !excludeKeys.includes(key)) { - return ( - <div key={uniqueId()} className="row"> - <div className="col-sm-3 text-right"> - <code className="code-import">{key}</code> - </div> - <div className="col-sm-9"> - { - Array.isArray(value) && <ul className="list-unstyled mb-0"> - { value.map(el => <li key={uniqueId()}> - <code className={codeClass[el.type]}>{el.uri}</code> - </li>) } - </ul> - } - { - !isUndefined(value.uri) && <code className={codeClass[value.type]}>{value.uri}</code> - } - { - isString(value) && <span>{truncate(value, {length: 512})}</span> - } - </div> - </div> - ) + {Object.entries(element) + .sort() + .map(([key, value]) => { + if (!excludeKeys.includes(key)) { + const serializedValue = serializeValue(value) + if (serializedValue !== '' || (element.changedFields?.includes(key))) { + return <FieldRow key={uniqueId()} element={element} keyName={key} value={serializedValue} /> + } } - }) - } + return null + })} </div> ) } Fields.propTypes = { - element: PropTypes.object.isRequired + element: PropTypes.object.isRequired, } export default Fields diff --git a/rdmo/management/assets/js/components/import/common/Form.js b/rdmo/management/assets/js/components/import/common/Form.js index a34892cc14..fa872b4efa 100644 --- a/rdmo/management/assets/js/components/import/common/Form.js +++ b/rdmo/management/assets/js/components/import/common/Form.js @@ -7,7 +7,6 @@ import UriPath from './UriPath' import UriPrefix from './UriPrefix' const Form = ({ config, element, updateElement }) => { - return ( <div className="row mt-10"> <div className="col-sm-6"> diff --git a/rdmo/management/assets/js/components/import/common/ImportFilters.js b/rdmo/management/assets/js/components/import/common/ImportFilters.js new file mode 100644 index 0000000000..8f5cc1b140 --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/ImportFilters.js @@ -0,0 +1,57 @@ +import React from 'react' +import PropTypes from 'prop-types' +import {FilterString, FilterUriPrefix} from '../../common/Filter' +import get from 'lodash/get' +import {getUriPrefixes} from '../../../utils/filter' +import {Checkbox} from '../../common/Checkboxes' + +const ImportFilters = ({ config, elements, changedElements, filteredElements, configActions, success= false}) => { + const updateFilterString = (value) => configActions.updateConfig('filter.import.elements.search', value) + const getValueFilterString = () => get(config, 'filter.import.elements.search', '') + const updateFilterUriPrefix = (value) => configActions.updateConfig('filter.import.elements.uri_prefix', value) + const getValueFilterUriPrefix = () => get(config, 'filter.import.elements.uri_prefix', '') + const updateFilterChanged = (value) => configActions.updateConfig('filter.import.elements.changed', value) + const getValueFilterChanged = () => get(config, 'filter.import.elements.changed', false) + + const filterCheckBoxText = interpolate( + success ? gettext('Show only created and changed elements (%s)') + : gettext('Show only new and changed elements (%s)'), + [changedElements.length] + ) + + return ( elements.length > 0 && + <div className="row"> + <div className={'col-sm-8'}> + <FilterString value={getValueFilterString()} onChange={updateFilterString} + placeholder={gettext('Filter URI')}/> + </div> + <div className="col-sm-4"> + <FilterUriPrefix value={getValueFilterUriPrefix()} + onChange={updateFilterUriPrefix} + options={getUriPrefixes(elements)}/> + </div> + + {elements.length > 0 && ( + <div className="horizontal-container"> + <div className="checkboxes"> + <Checkbox label={filterCheckBoxText} + value={getValueFilterChanged()} onChange={updateFilterChanged} /> + </div> + <span className="shown-info"> + {gettext('Shown')}: {filteredElements.length} / {elements.length} + </span> + </div> + )} + </div>) +} + +ImportFilters.propTypes = { + config: PropTypes.object.isRequired, + elements: PropTypes.array.isRequired, + changedElements: PropTypes.array.isRequired, + filteredElements: PropTypes.array.isRequired, + configActions: PropTypes.object.isRequired, + success: PropTypes.bool +} + +export default ImportFilters diff --git a/rdmo/management/assets/js/components/import/common/ImportInfo.js b/rdmo/management/assets/js/components/import/common/ImportInfo.js new file mode 100644 index 0000000000..d1fbf9722b --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/ImportInfo.js @@ -0,0 +1,44 @@ +import React from 'react' +import PropTypes from 'prop-types' +import {isUndefined} from 'lodash' + +const renderElementLengthInfo = (label, length) => ( + length > 0 && ( + <span className='ml-5'>{gettext(label)}: {length} </span> + ) +) + +const ImportInfo = ({ + elementsLength, + updatedLength, + createdLength, + changedLength, + warningsLength, + errorsLength + }) => { + if (isUndefined(elementsLength) || elementsLength === 0) { + return null + } + + return ( + <div className="pull-right"> + {renderElementLengthInfo('Total', elementsLength)} + {renderElementLengthInfo('Updated', updatedLength)} + {renderElementLengthInfo('Changed', changedLength)} + {renderElementLengthInfo('Created', createdLength)} + {renderElementLengthInfo('Warnings', warningsLength)} + {renderElementLengthInfo('Errors', errorsLength)} + </div> + ) +} + +ImportInfo.propTypes = { + elementsLength: PropTypes.number, + updatedLength: PropTypes.number, + createdLength: PropTypes.number, + changedLength: PropTypes.number, + warningsLength: PropTypes.number, + errorsLength: PropTypes.number, +} + +export default ImportInfo diff --git a/rdmo/management/assets/js/components/import/common/ImportLabels.js b/rdmo/management/assets/js/components/import/common/ImportLabels.js new file mode 100644 index 0000000000..b44fb7b3f1 --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/ImportLabels.js @@ -0,0 +1,33 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Label from 'rdmo/management/assets/js/components/common/Labels' + +const ChangedLabel = ({ text, onClick, show }) => { + return <Label text={text} + type="info" + onClick={onClick} + className={'ml-5'} + show={show} /> +} + +ChangedLabel.propTypes = { + text: PropTypes.string.isRequired, + onClick: PropTypes.func, + show: PropTypes.bool, +} + +const CreatedLabel = ({ text, onClick, show }) => { + return <Label text={text} + type="success" + onClick={onClick} + className={'ml-5'} + show={show} /> +} + +CreatedLabel.propTypes = { + text: PropTypes.string.isRequired, + onClick: PropTypes.func, + show: PropTypes.bool, +} + +export { ChangedLabel, CreatedLabel } diff --git a/rdmo/management/assets/js/components/import/common/ImportSelectCheckbox.js b/rdmo/management/assets/js/components/import/common/ImportSelectCheckbox.js new file mode 100644 index 0000000000..53f4189301 --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/ImportSelectCheckbox.js @@ -0,0 +1,30 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { CodeLink } from '../../common/Links' +import { codeClass, verboseNames } from '../../../constants/elements' +import {ChangedLabel, CreatedLabel} from './ImportLabels' + +const ImportSelectCheckbox = ({ element, toggleImport, updateShowField }) => { + const changedLabelText = gettext('Changed') + const createdLabelText = gettext('New') + return ( + <div className="checkbox"> + <label className="mr-5"> + <input type="checkbox" checked={element.import} onChange={toggleImport} /> + <strong>{verboseNames[element.model]}{' '}</strong> + </label> + <CodeLink className={codeClass[element.model]} uri={element.uri} onClick={updateShowField} /> + + <ChangedLabel text={changedLabelText} show={(element.changed && element.updated)} /> + + <CreatedLabel text={createdLabelText} show={element.created} /> + </div> +)} + +ImportSelectCheckbox.propTypes = { + element: PropTypes.object.isRequired, + toggleImport: PropTypes.func.isRequired, + updateShowField: PropTypes.func.isRequired +} + +export default ImportSelectCheckbox diff --git a/rdmo/management/assets/js/components/import/common/Key.js b/rdmo/management/assets/js/components/import/common/Key.js index e6396b0098..fe63bac327 100644 --- a/rdmo/management/assets/js/components/import/common/Key.js +++ b/rdmo/management/assets/js/components/import/common/Key.js @@ -4,7 +4,7 @@ import uniqueId from 'lodash/uniqueId' const Key = ({ element, onChange }) => { const id = uniqueId('key-'), - value = element.key + value = element.key ?? '' return ( <div className="form-group mb-0"> diff --git a/rdmo/management/assets/js/components/import/common/UriPath.js b/rdmo/management/assets/js/components/import/common/UriPath.js index 70ebe4ccaf..62e9d99be1 100644 --- a/rdmo/management/assets/js/components/import/common/UriPath.js +++ b/rdmo/management/assets/js/components/import/common/UriPath.js @@ -2,9 +2,9 @@ import React from 'react' import PropTypes from 'prop-types' import uniqueId from 'lodash/uniqueId' -const UriPrefix = ({ element, onChange }) => { - const id = uniqueId('uriPrefix-'), - value = element.uri_path +const UriPath = ({ element, onChange }) => { + const id = uniqueId('uriPath-'), + value = element.uri_path ?? '' return ( <div className="form-group mb-0"> @@ -18,9 +18,9 @@ const UriPrefix = ({ element, onChange }) => { ) } -UriPrefix.propTypes = { +UriPath.propTypes = { element: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired } -export default UriPrefix +export default UriPath diff --git a/rdmo/management/assets/js/components/import/common/Warnings.js b/rdmo/management/assets/js/components/import/common/Warnings.js index c4c91bdb83..2ecfc75ab1 100644 --- a/rdmo/management/assets/js/components/import/common/Warnings.js +++ b/rdmo/management/assets/js/components/import/common/Warnings.js @@ -1,25 +1,28 @@ import React from 'react' import PropTypes from 'prop-types' -import isEmpty from 'lodash/isEmpty' -import uniqueId from 'lodash/uniqueId' +import WarningsListGroup from './WarningsListGroup' +import isUndefined from 'lodash/isUndefined' -const Warnings = ({ element }) => { - return !isEmpty(element.warnings) && <div className="row text-warning mt-10"> - <div className="col-sm-3 text-right"> - {gettext('Warnings')} - </div> - <div className="col-sm-9"> - <ul className="list-unstyled"> - { - element.warnings.map(message => <li key={uniqueId('error-')}>{message}</li>) - } - </ul> +const Warnings = ({elementWarnings, elementModel, shouldShowURI = true}) => { + const show = !isUndefined(elementWarnings) && Object.keys(elementWarnings).length > 0 + const warningsHeadingText = <strong>{gettext('Warnings')}</strong> + + return show && ( + <div className="panel panel-warning mt-10 mb-0"> + <div className="panel-heading">{warningsHeadingText}</div> + <WarningsListGroup + elementWarnings={elementWarnings} + elementModel={elementModel} + shouldShowURI={shouldShowURI} + /> </div> - </div> + ) } Warnings.propTypes = { - element: PropTypes.object.isRequired + elementWarnings: PropTypes.object.isRequired, + elementModel: PropTypes.string.isRequired, + shouldShowURI: PropTypes.bool } export default Warnings diff --git a/rdmo/management/assets/js/components/import/common/WarningsListGroup.js b/rdmo/management/assets/js/components/import/common/WarningsListGroup.js new file mode 100644 index 0000000000..efcfb665f5 --- /dev/null +++ b/rdmo/management/assets/js/components/import/common/WarningsListGroup.js @@ -0,0 +1,40 @@ +// WarningsListGroup.js +import React from 'react' +import PropTypes from 'prop-types' +import uniqueId from 'lodash/uniqueId' +import { codeClass, verboseNames } from '../../../constants/elements' + +// Helper function to generate warning messages +export const generateWarningListItems = (elementWarnings, elementModel, shouldShowURI = true) => + Object.entries(elementWarnings).flatMap(([uri, messages]) => + messages.map(message => ( + <li className="list-group-item" key={uniqueId('warning-uri-message')}> + {shouldShowURI && ( + <> + <strong>{verboseNames[elementModel]}{' '}</strong> + <code className={codeClass[elementModel]}>{uri}</code> + <br /> + </> + )} + <div className="text-warning"> + {message} + </div> + </li> + )) + ) + +const WarningsListGroup = ({ elementWarnings, elementModel, shouldShowURI }) => { + return ( + <ul className="list-group"> + {generateWarningListItems(elementWarnings, elementModel, shouldShowURI)} + </ul> + ) +} + +WarningsListGroup.propTypes = { + elementWarnings: PropTypes.object.isRequired, + elementModel: PropTypes.string.isRequired, + shouldShowURI: PropTypes.bool +} + +export default WarningsListGroup diff --git a/rdmo/management/assets/js/components/main/Import.js b/rdmo/management/assets/js/components/main/Import.js index 6f93e80fdc..8f27c8d35d 100644 --- a/rdmo/management/assets/js/components/main/Import.js +++ b/rdmo/management/assets/js/components/main/Import.js @@ -1,94 +1,83 @@ import React from 'react' import PropTypes from 'prop-types' -import uniqueId from 'lodash/uniqueId' -import isEmpty from 'lodash/isEmpty' +import get from 'lodash/get' -import ImportAttribute from '../import/ImportAttribute' -import ImportCatalog from '../import/ImportCatalog' -import ImportCondition from '../import/ImportCondition' -import ImportOption from '../import/ImportOption' -import ImportOptionSet from '../import/ImportOptionSet' -import ImportPage from '../import/ImportPage' -import ImportQuestion from '../import/ImportQuestion' -import ImportQuestionSet from '../import/ImportQuestionSet' -import ImportSection from '../import/ImportSection' -import ImportTask from '../import/ImportTask' -import ImportView from '../import/ImportView' +import ImportElement from '../import/ImportElement' +import ImportSuccessElement from '../import/ImportSuccessElement' +import ImportAggregatedWarningsPanel from '../import/ImportAggregatedWarningsPanel' +import ImportAggregatedErrorsPanel from '../import/ImportAggregatedErrorsPanel' +import ImportInfo from '../import/common/ImportInfo' +import ImportFilters from '../import/common/ImportFilters' +import useFilteredElements from '../../utils/importFilters' +import {useImportElements} from '../../hooks/useImportElements' -import { codeClass, verboseNames } from '../../constants/elements' -const Import = ({ config, imports, importActions }) => { - const { elements, success } = imports +const Import = ({ config, imports, configActions, importActions }) => { + const { file, elements, success } = imports + // the elements are already processed by processElementDiffs in the importsReducer + const { + createdElements, + updatedElements, + changedElements, + importWarnings, + importErrors + } = useImportElements(elements) + + const searchString = get(config, 'filter.import.elements.search', '') + const selectedUriPrefix = get(config, 'filter.import.elements.uri_prefix', '') + const selectFilterChanged = get(config, 'filter.import.elements.changed', false) + + const filteredElements = useFilteredElements(elements, selectFilterChanged, selectedUriPrefix, searchString) return ( - <div className="panel panel-default panel-import"> - <div className="panel-heading"> - <strong>{gettext('Import')}</strong> - </div> + <div className='panel panel-default panel-import'> + <div className='panel-heading'> + <strong>{gettext('Import')} from: {file.name}</strong> + <ImportInfo elementsLength={elements.length} createdLength={createdElements.length} + updatedLength={updatedElements.length} changedLength={changedElements.length} + warningsLength={importWarnings.length} errorsLength={importErrors.length}/> - <ul className="list-group"> - { - elements.map((element, index) => { - if (success) { - return ( - <li key={index} className="list-group-item"> - <p> - <strong>{verboseNames[element.model]}{' '}</strong> - <code className={codeClass[element.model]}>{element.uri}</code> - {element.created && <span className="text-success">{' '}{gettext('created')}</span>} - {element.updated && <span className="text-info">{' '}{gettext('updated')}</span>} - { - !isEmpty(element.errors) && !(element.created || element.updated) && - <span className="text-danger">{' '}{gettext('could not be imported')}</span> - } - { - !isEmpty(element.errors) && (element.created || element.updated) && - <>{', '}<span className="text-danger">{gettext('but could not be added to parent element')}</span></> - } - {'.'} - </p> - {element.warnings.map(message => <p key={uniqueId()} className="text-warning">{message}</p>)} - {element.errors.map(message => <p key={uniqueId()} className="text-danger">{message}</p>)} - </li> - ) - } else { - switch (element.model) { - case 'questions.catalog': - return <ImportCatalog key={index} config={config} catalog={element} importActions={importActions} /> - case 'questions.section': - return <ImportSection key={index} config={config} section={element} importActions={importActions} /> - case 'questions.page': - return <ImportPage key={index} config={config} page={element} importActions={importActions} /> - case 'questions.questionset': - return <ImportQuestionSet key={index} config={config} questionset={element} importActions={importActions} /> - case 'questions.question': - return <ImportQuestion key={index} config={config} question={element} importActions={importActions} /> - case 'domain.attribute': - return <ImportAttribute key={index} config={config} attribute={element} importActions={importActions} /> - case 'options.optionset': - return <ImportOptionSet key={index} config={config} optionset={element} importActions={importActions} /> - case 'options.option': - return <ImportOption key={index} config={config} option={element} importActions={importActions} /> - case 'conditions.condition': - return <ImportCondition key={index} config={config} condition={element} importActions={importActions} /> - case 'tasks.task': - return <ImportTask key={index} config={config} task={element} importActions={importActions} /> - case 'views.view': - return <ImportView key={index} config={config} view={element} importActions={importActions} /> - default: - return null - } + </div> + <div className="panel-body"> + { + <ImportFilters config={config} elements={elements} + changedElements={changedElements} + filteredElements={filteredElements} + configActions={configActions} + success={success} /> + } + { + <ImportAggregatedWarningsPanel config={config} elements={importWarnings} + importActions={importActions} + configActions={configActions} /> + } + { + <ImportAggregatedErrorsPanel config={config} elements={importErrors} + importActions={importActions} + configActions={configActions} /> + } + </div> + <ul className='list-group'> + { + filteredElements.map((element, index) => { + if (success) { + return <ImportSuccessElement key={index} element={element} + importActions={importActions}/> + } else { + return <ImportElement key={index} element={element} config={config} + importActions={importActions}/> + } + }) } - }) - } - </ul> - </div> + </ul> + </div> ) } Import.propTypes = { config: PropTypes.object.isRequired, imports: PropTypes.object.isRequired, + configActions: PropTypes.object.isRequired, importActions: PropTypes.object.isRequired } diff --git a/rdmo/management/assets/js/components/nested/NestedCatalog.js b/rdmo/management/assets/js/components/nested/NestedCatalog.js index b57840b9c6..3a82efef8b 100644 --- a/rdmo/management/assets/js/components/nested/NestedCatalog.js +++ b/rdmo/management/assets/js/components/nested/NestedCatalog.js @@ -19,9 +19,9 @@ const NestedCatalog = ({ config, catalog, configActions, elementActions }) => { const updateFilterString = (value) => configActions.updateConfig('filter.catalog.search', value) const updateFilterUriPrefix = (value) => configActions.updateConfig('filter.catalog.uri_prefix', value) - const toggleSections = () => configActions.toggleDescandants(catalog, 'sections') - const togglePages = () => configActions.toggleDescandants(catalog, 'pages') - const toggleQuestionSets = () => configActions.toggleDescandants(catalog, 'questionsets') + const toggleSections = () => configActions.toggleDescendants(catalog, 'sections') + const togglePages = () => configActions.toggleDescendants(catalog, 'pages') + const toggleQuestionSets = () => configActions.toggleDescendants(catalog, 'questionsets') const updateDisplayCatalogURI = (value) => configActions.updateConfig('display.uri.catalogs', value) const updateDisplaySectionsURI = (value) => configActions.updateConfig('display.uri.sections', value) diff --git a/rdmo/management/assets/js/components/nested/NestedPage.js b/rdmo/management/assets/js/components/nested/NestedPage.js index 801ddc4640..08751f25a2 100644 --- a/rdmo/management/assets/js/components/nested/NestedPage.js +++ b/rdmo/management/assets/js/components/nested/NestedPage.js @@ -21,7 +21,7 @@ const NestedPage = ({ config, page, configActions, elementActions }) => { const updateFilterString = (uri) => configActions.updateConfig('filter.page.search', uri) const updateFilterUriPrefix = (uriPrefix) => configActions.updateConfig('filter.page.uri_prefix', uriPrefix) - const toggleQuestionSets = () => configActions.toggleDescandants(page, 'questionsets') + const toggleQuestionSets = () => configActions.toggleDescendants(page, 'questionsets') const updateDisplayPagesURI = (value) => configActions.updateConfig('display.uri.pages', value) const updateDisplayQuestionSetsURI = (value) => configActions.updateConfig('display.uri.questionsets', value) diff --git a/rdmo/management/assets/js/components/nested/NestedQuestionSet.js b/rdmo/management/assets/js/components/nested/NestedQuestionSet.js index 70db94a5e0..debed7a6c6 100644 --- a/rdmo/management/assets/js/components/nested/NestedQuestionSet.js +++ b/rdmo/management/assets/js/components/nested/NestedQuestionSet.js @@ -20,7 +20,7 @@ const NestedQuestionSet = ({ config, questionset, configActions, elementActions const updateFilterString = (uri) => configActions.updateConfig('filter.questionset.search', uri) const updateFilterUriPrefix = (uriPrefix) => configActions.updateConfig('filter.questionset.uri_prefix', uriPrefix) - const toggleQuestionSets = () => configActions.toggleDescandants(questionset, 'questionsets') + const toggleQuestionSets = () => configActions.toggleDescendants(questionset, 'questionsets') const updateDisplayQuestionSetsURI = (value) => configActions.updateConfig('display.uri.questionsets', value) const updateDisplayQuestionsURI = (value) => configActions.updateConfig('display.uri.questions', value) diff --git a/rdmo/management/assets/js/components/nested/NestedSection.js b/rdmo/management/assets/js/components/nested/NestedSection.js index 1552ddd0bc..9fa29b6559 100644 --- a/rdmo/management/assets/js/components/nested/NestedSection.js +++ b/rdmo/management/assets/js/components/nested/NestedSection.js @@ -20,8 +20,8 @@ const NestedCatalog = ({ config, section, configActions, elementActions }) => { const updateFilterString = (uri) => configActions.updateConfig('filter.section.search', uri) const updateFilterUriPrefix = (uriPrefix) => configActions.updateConfig('filter.section.uri_prefix', uriPrefix) - const togglePages = () => configActions.toggleDescandants(section, 'pages') - const toggleQuestionSets = () => configActions.toggleDescandants(section, 'questionsets') + const togglePages = () => configActions.toggleDescendants(section, 'pages') + const toggleQuestionSets = () => configActions.toggleDescendants(section, 'questionsets') const updateDisplaySectionURI = (value) => configActions.updateConfig('display.uri.sections', value) const updateDisplayPagesURI = (value) => configActions.updateConfig('display.uri.pages', value) diff --git a/rdmo/management/assets/js/components/sidebar/ElementsSidebar.js b/rdmo/management/assets/js/components/sidebar/ElementsSidebar.js index 65420eab49..88e92cd84b 100644 --- a/rdmo/management/assets/js/components/sidebar/ElementsSidebar.js +++ b/rdmo/management/assets/js/components/sidebar/ElementsSidebar.js @@ -3,8 +3,6 @@ import PropTypes from 'prop-types' import isNil from 'lodash/isNil' import invert from 'lodash/invert' -import baseUrl from 'rdmo/core/assets/js/utils/baseUrl' - import { elementTypes, elementModules } from '../../constants/elements' import { buildPath } from '../../utils/location' @@ -18,8 +16,8 @@ const ElementsSidebar = ({ config, elements, elementActions, importActions }) => const { elementType, elementId } = elements const model = invert(elementTypes)[elementType] - const exportUrl = isNil(elementId) ? `${baseUrl}/api/v1/${elementModules[model]}/${elementType}/export/` - : `${baseUrl}/api/v1/${elementModules[model]}/${elementType}/${elementId}/export/` + const exportUrl = isNil(elementId) ? buildPath(config.apiUrl, elementModules[model], elementType, 'export') + : buildPath(config.apiUrl, elementModules[model], elementType, elementId, 'export') const exportParams = getExportParams(config.filter[elementType]) return ( @@ -75,10 +73,33 @@ const ElementsSidebar = ({ config, elements, elementActions, importActions }) => <h2>Export</h2> + <p className="text-muted"> + {gettext('Export all visible elements.')} + </p> + <ul className="list-unstyled"> <li> <a href={`${exportUrl}?${exportParams}`}>{gettext('XML')}</a> </li> + { + [ + 'catalogs', + 'sections', + 'pages', + 'questionsets', + 'questions', + 'optionsets', + 'conditions', + 'tasks' + ].includes(elementType) && ( + <li> + <a href={`${exportUrl}?full=true&${exportParams}`}>{gettext('XML (full)')}</a> + </li> + ) + } + </ul> + + <ul className="list-unstyled"> { elementType == 'attributes' && <> <li> @@ -105,6 +126,10 @@ const ElementsSidebar = ({ config, elements, elementActions, importActions }) => <h2>Import</h2> + <p className="text-muted"> + {gettext('Import an RDMO XML file.')} + </p> + <UploadForm onSubmit={file => importActions.uploadFile(file)} /> </div> ) diff --git a/rdmo/management/assets/js/components/sidebar/ImportSidebar.js b/rdmo/management/assets/js/components/sidebar/ImportSidebar.js index 9d27019d28..176ccb0413 100644 --- a/rdmo/management/assets/js/components/sidebar/ImportSidebar.js +++ b/rdmo/management/assets/js/components/sidebar/ImportSidebar.js @@ -4,9 +4,16 @@ import isEmpty from 'lodash/isEmpty' import isNil from 'lodash/isNil' import Link from 'rdmo/core/assets/js/components/Link' +import {useImportElements} from '../../hooks/useImportElements' + const ImportSidebar = ({ config, imports, importActions }) => { const { elements, success } = imports + + const { + changedElements, + } = useImportElements(elements) + const count = elements.filter(e => e.import).length const [uriPrefix, setUriPrefix] = useState('') const disabled = isNil(uriPrefix) || isEmpty(uriPrefix) @@ -33,7 +40,6 @@ const ImportSidebar = ({ config, imports, importActions }) => { return ( <div className="import-sidebar"> <h2>{gettext('Import elements')}</h2> - <p className="import-buttons"> <button className="btn btn-success" onClick={() => importActions.importElements()}> {interpolate(ngettext('Import one element', 'Import %s elements', count), [count])} @@ -51,13 +57,55 @@ const ImportSidebar = ({ config, imports, importActions }) => { {gettext('Select all')} </Link> </li> + {changedElements.length > 0 && + <li> + <Link onClick={() => importActions.selectChangedElements(true)}> + {gettext('Select changed')} + </Link> + </li> + } <li> <Link onClick={() => importActions.selectElements(false)}> - {gettext('Unselect all')} + {gettext('Deselect all')} </Link> </li> + {changedElements.length > 0 && + <li> + <Link onClick={() => importActions.selectChangedElements(false)}> + {gettext('Deselect changed')} + </Link> + </li> + } </ul> + <h2>{gettext('Show')}</h2> + <ul className="list-unstyled"> + <li> + <Link onClick={() => importActions.showElements(true)}> + {gettext('Show all')} + </Link> + </li> + {changedElements.length > 0 && + <li> + <Link onClick={() => importActions.showChangedElements(true)}> + {gettext('Show changes')} + </Link> + </li> + } + <li> + <Link onClick={() => importActions.showElements(false)}> + {gettext('Hide all')} + </Link> + </li> + {changedElements.length > 0 && + <li> + <Link onClick={() => importActions.showChangedElements(false)}> + {gettext('Hide changes')} + </Link> + </li> + } + </ul> + <h2>{gettext('URI prefix')}</h2> <div className="form-group"> diff --git a/rdmo/management/assets/js/constants/elements.js b/rdmo/management/assets/js/constants/elements.js index 9c32d008a1..112dd13fa8 100644 --- a/rdmo/management/assets/js/constants/elements.js +++ b/rdmo/management/assets/js/constants/elements.js @@ -4,7 +4,7 @@ const elementTypes = { 'questions.page': 'pages', 'questions.questionset': 'questionsets', 'questions.question': 'questions', - 'domain.attribute':'attributes', + 'domain.attribute': 'attributes', 'options.optionset': 'optionsets', 'options.option': 'options', 'conditions.condition': 'conditions', diff --git a/rdmo/management/assets/js/containers/Main.js b/rdmo/management/assets/js/containers/Main.js index 4cd6049d3f..532b1fdb75 100644 --- a/rdmo/management/assets/js/containers/Main.js +++ b/rdmo/management/assets/js/containers/Main.js @@ -25,7 +25,7 @@ const Main = ({ config, elements, imports, configActions, elementActions, import return null } - // check if an an error occurred + // check if an error occurred if (!isNil(elements.errors.api)) { return <MainErrors errors={elements.errors.api} /> } else if (get(elements, 'element.errors.api')) { @@ -35,11 +35,12 @@ const Main = ({ config, elements, imports, configActions, elementActions, import } if (!isEmpty(imports.elements)) { - return <Import config={config} imports={imports} importActions={importActions} /> + return <Import config={config} imports={imports} + configActions={configActions} importActions={importActions} /> } // check if the nested components should be displayed - if (!isNil(element) && elementAction == 'nested') { + if (!isNil(element) && elementAction === 'nested') { return <Nested config={config} elements={elements} configActions={configActions} elementActions={elementActions} /> } diff --git a/rdmo/management/assets/js/factories/OptionsFactory.js b/rdmo/management/assets/js/factories/OptionsFactory.js index a0b6436193..50bb461e4e 100644 --- a/rdmo/management/assets/js/factories/OptionsFactory.js +++ b/rdmo/management/assets/js/factories/OptionsFactory.js @@ -13,6 +13,7 @@ class OptionsFactory { return { model: 'options.option', uri_prefix: config.settings.default_uri_prefix, + uri_path: parent.optionset ? parent.optionset.uri_path : '', optionsets: parent.optionset ? [parent.optionset.id] : [], conditions: [], editors: config.settings.multisite ? [config.currentSite.id] : [], diff --git a/rdmo/management/assets/js/factories/QuestionsFactory.js b/rdmo/management/assets/js/factories/QuestionsFactory.js index bb8d6a47f6..47547f6af6 100644 --- a/rdmo/management/assets/js/factories/QuestionsFactory.js +++ b/rdmo/management/assets/js/factories/QuestionsFactory.js @@ -15,6 +15,7 @@ class QuestionsFactory { return { model: 'questions.section', uri_prefix: config.settings.default_uri_prefix, + uri_path: parent.catalog ? parent.catalog.uri_path : '', catalogs: parent.catalog ? [parent.catalog.id] : [], pages: [], editors: config.settings.multisite ? [config.currentSite.id] : [], @@ -25,6 +26,7 @@ class QuestionsFactory { return { model: 'questions.page', uri_prefix: config.settings.default_uri_prefix, + uri_path: parent.section ? parent.section.uri_path : '', sections: parent.section ? [parent.section.id] : [], questionsets: [], questions: [], @@ -36,6 +38,9 @@ class QuestionsFactory { return { model: 'questions.questionset', uri_prefix: config.settings.default_uri_prefix, + uri_path: parent.page ? parent.page.uri_path : ( + parent.questionset ? parent.questionset.uri_path : '' + ), pages: parent.page ? [parent.page.id] : [], parents: parent.questionset ? [parent.questionset.id] : [], questionsets: [], @@ -48,6 +53,9 @@ class QuestionsFactory { return { model: 'questions.question', uri_prefix: config.settings.default_uri_prefix, + uri_path: parent.page ? parent.page.uri_path : ( + parent.questionset ? parent.questionset.uri_path : '' + ), widget_type: 'text', value_type: 'text', pages: parent.page ? [parent.page.id] : [], diff --git a/rdmo/management/assets/js/hooks/useImportElements.js b/rdmo/management/assets/js/hooks/useImportElements.js new file mode 100644 index 0000000000..9fa3492796 --- /dev/null +++ b/rdmo/management/assets/js/hooks/useImportElements.js @@ -0,0 +1,22 @@ +import {useMemo} from 'react' +import isEmpty from 'lodash/isEmpty' + +export const useImportElements = (elements) => { + return useMemo(() => { + // the elements are already processed by processElementDiffs in the importsReducer + const createdElements = elements.filter(element => element.created) + const updatedElements = elements.filter(element => element.updated) + // changedElements collects elements with updated AND changed OR created + const changedElements = elements.filter(element => ((element.updated && element.changed) || element.created)) + const importWarnings = elements.filter(element => !isEmpty(element.warnings)) + const importErrors = elements.filter(element => !isEmpty(element.errors)) + + return { + createdElements, + updatedElements, + changedElements, + importWarnings, + importErrors + } + }, [elements]) +} diff --git a/rdmo/management/assets/js/reducers/configReducer.js b/rdmo/management/assets/js/reducers/configReducer.js index 4eb2500380..31029c17f0 100644 --- a/rdmo/management/assets/js/reducers/configReducer.js +++ b/rdmo/management/assets/js/reducers/configReducer.js @@ -4,6 +4,7 @@ import baseUrl from 'rdmo/core/assets/js/utils/baseUrl' const initialState = { baseUrl: baseUrl + '/management/', + apiUrl: baseUrl + '/api/v1/', settings: {}, filter: {}, display: {} diff --git a/rdmo/management/assets/js/reducers/elementsReducer.js b/rdmo/management/assets/js/reducers/elementsReducer.js index 02082a6f25..fc8f62d982 100644 --- a/rdmo/management/assets/js/reducers/elementsReducer.js +++ b/rdmo/management/assets/js/reducers/elementsReducer.js @@ -32,6 +32,7 @@ export default function elementsReducer(state = initialState, action) { elementId: null, elementAction: null, element: null, + parent: null, errors: {} } case 'elements/fetchElementsSuccess': @@ -46,6 +47,7 @@ export default function elementsReducer(state = initialState, action) { elementId: action.elementId, elementAction: action.elementAction, element: null, + parent: null, errors: {} } case 'elements/fetchElementSuccess': @@ -89,6 +91,7 @@ export default function elementsReducer(state = initialState, action) { elementId: null, elementAction: 'create', element: null, + parent: null, errors: {} } case 'elements/createElementSuccess': diff --git a/rdmo/management/assets/js/reducers/importsReducer.js b/rdmo/management/assets/js/reducers/importsReducer.js index 5b6d115fa6..28810fddaf 100644 --- a/rdmo/management/assets/js/reducers/importsReducer.js +++ b/rdmo/management/assets/js/reducers/importsReducer.js @@ -2,13 +2,15 @@ import isArray from 'lodash/isArray' import isNil from 'lodash/isNil' import isUndefined from 'lodash/isUndefined' -import { buildUri } from '../utils/elements' +import { buildUri, buildPathForAttribute } from '../utils/elements' +import processElementDiffs from '../utils/processElementDiffs' const initialState = { elements: [], errors: [], - success: false + success: false, + file: null } export default function importsReducer(state = initialState, action) { @@ -17,11 +19,13 @@ export default function importsReducer(state = initialState, action) { switch(action.type) { // upload file case 'import/uploadFileInit': + return {...state, ...initialState, file: action.file} case 'elements/fetchElementsInit': case 'elements/fetchElementInit': return {...state, elements: [], errors: [], success: false} case 'import/uploadFileSuccess': return {...state, elements: action.elements.map(element => { + element = processElementDiffs(element) if (['questions.catalogs', 'tasks.task', 'views.view'].includes(element.model)) { element.available = true } @@ -40,25 +44,56 @@ export default function importsReducer(state = initialState, action) { // update element case 'import/updateElement': - index = state.elements.findIndex(element => element == action.element) + index = state.elements.findIndex(element => element === action.element) if (index > -1) { const elements = [...state.elements] elements[index] = {...elements[index], ...action.values} - elements[index].uri = buildUri(elements[index]) + if (elements[index].model === 'domain.attribute') { + elements[index].path = buildPathForAttribute(elements[index].key, elements[index].parent ? elements[index].parent.uri : null) + } + const newUri = buildUri(elements[index]) + if (!isNil(newUri)) { + elements[index].uri = newUri + } return {...state, elements} } else { return state } case 'import/selectElements': return {...state, elements: state.elements.map(element => { - return {...element, import: action.value} + return {...element, import: action.value} })} + case 'import/selectChangedElements': + return {...state, elements: state.elements.map(element => { + if (element.changed || element.created ) { + return {...element, import: action.value} + } + else if (action.value) {return {...element, import: !action.value}} + else { return element } + } + )} + case 'import/showElements': + return {...state, elements: state.elements.map(element => { + return {...element, show: action.value} + })} + case 'import/showChangedElements': + return {...state, elements: state.elements.map(element => { + if (element.changed || element.created ) { + return {...element, show: action.value} + } + else if (action.value) {return {...element, show: !action.value}} + else { return element } + } + )} case 'import/updateUriPrefix': elements = state.elements.map(element => { element.uri_prefix = action.uriPrefix // compute a new uri and store it in the elementMap - element.uri = elementsMap[element.uri] = buildUri(element) + const newUri = buildUri(element) + if (!isNil(newUri)) { + element.uri = elementsMap[element.uri] = newUri + } return element }) diff --git a/rdmo/management/assets/js/store/configureStore.js b/rdmo/management/assets/js/store/configureStore.js index f96d73cc06..7f5e3cd224 100644 --- a/rdmo/management/assets/js/store/configureStore.js +++ b/rdmo/management/assets/js/store/configureStore.js @@ -40,19 +40,21 @@ export default function configureStore() { const updateConfigFromLocalStorage = () => { const ls = {...localStorage} Object.entries(ls).forEach(([lsPath, lsValue]) => { - const path = lsPath.replace('rdmo.management.config.', '') - let value - switch(lsValue) { - case 'true': - value = true - break - case 'false': - value = false - break - default: - value = lsValue + if (lsPath.startsWith('rdmo.management.config.')) { + const path = lsPath.replace('rdmo.management.config.', '') + let value + switch(lsValue) { + case 'true': + value = true + break + case 'false': + value = false + break + default: + value = lsValue + } + store.dispatch(configActions.updateConfig(path, value)) } - store.dispatch(configActions.updateConfig(path, value)) }) } diff --git a/rdmo/management/assets/js/utils/elements.js b/rdmo/management/assets/js/utils/elements.js index 8c006c5c48..f9be3aa889 100644 --- a/rdmo/management/assets/js/utils/elements.js +++ b/rdmo/management/assets/js/utils/elements.js @@ -162,17 +162,38 @@ function findDescendants(element, elementType) { } const buildUri = (element) => { - let uri = element.uri_prefix + '/' + elementModules[element.model] + '/' + if (isUndefined(element.uri_prefix) || isUndefined(element.model)) { + return null + } + + let uri = `${element.uri_prefix}/${elementModules[element.model]}/` - if (!isUndefined(element.uri_path)) { + if (!isUndefined(element.uri_path) && !isNil(element.uri_path)) { uri += element.uri_path - } else if (!isUndefined(element.path)) { + } else if (!isUndefined(element.path) && !isNil(element.path)) { uri += element.path - } else { + } else if (!isUndefined(element.key) && !isNil(element.key)) { uri += element.key + } else { + return null } return uri } -export { compareElements, updateElement, resetElement, canMoveElement, moveElement, findDescendants, buildUri } +const buildPathForAttribute = (key, parentUri) => { + let path = key + if (parentUri) { + if (parentUri.includes('/domain/')) { + // construct the path using parentUri directly + const parentPath = parentUri.split('/domain/')[1] + path = parentPath ? `${parentPath}/${key}` : key + } + } + + return path +} + + + +export { compareElements, updateElement, resetElement, canMoveElement, moveElement, findDescendants, buildUri, buildPathForAttribute } diff --git a/rdmo/management/assets/js/utils/filter.js b/rdmo/management/assets/js/utils/filter.js index 324d0c6fed..5b5a318810 100644 --- a/rdmo/management/assets/js/utils/filter.js +++ b/rdmo/management/assets/js/utils/filter.js @@ -37,6 +37,7 @@ const filterEditor = (editor, element) => { return isEmpty(editor) || element.editors.includes(toNumber(editor)) } + const getUriPrefixes = (elements) => { return elements.reduce((acc, cur) => { if (!acc.includes(cur.uri_prefix)) { @@ -61,4 +62,4 @@ const getExportParams = (filter) => { return exportParams.toString() } -export { filterElement, getUriPrefixes, getExportParams } +export { filterElement, getUriPrefixes, getExportParams, filterUriPrefix, filterSearch } diff --git a/rdmo/management/assets/js/utils/getDiff.js b/rdmo/management/assets/js/utils/getDiff.js new file mode 100644 index 0000000000..dcafde9f63 --- /dev/null +++ b/rdmo/management/assets/js/utils/getDiff.js @@ -0,0 +1,30 @@ +import { DiffMethod } from 'react-diff-viewer-continued' + +function getDiff(currentData, updatedData) { + let originalValueStr = currentData || '' + let newValueStr = updatedData || '' + let compareMethod = DiffMethod.CHARS + + if (Array.isArray(originalValueStr) && Array.isArray(newValueStr)) { + // Cast array to string, joined by newline + originalValueStr = originalValueStr.join('\n') + newValueStr = newValueStr.join('\n') + compareMethod = DiffMethod.LINES + } else { + // Cast to string + originalValueStr = originalValueStr.toString() + newValueStr = newValueStr.toString() + } + + const changed = newValueStr !== originalValueStr + + // Return a structured object + return { + oldValue: originalValueStr, + newValue: newValueStr, + changed: changed, + compareMethod: compareMethod + } +} + +export default getDiff diff --git a/rdmo/management/assets/js/utils/importFilters.js b/rdmo/management/assets/js/utils/importFilters.js new file mode 100644 index 0000000000..a5fc35562d --- /dev/null +++ b/rdmo/management/assets/js/utils/importFilters.js @@ -0,0 +1,32 @@ +import { useMemo } from 'react' +import { filterUriPrefix, filterSearch} from './filter' + +const filterChanged = (selectFilterChanged, element) => { + return element.changed || element.created +} + +function filterElementsByChanged(elements, selectFilterChanged) { + if (!selectFilterChanged) return elements + return elements.filter((element) => filterChanged(selectFilterChanged, element)) +} + +function filterElementsByUri(elements, searchString) { + if (!searchString) return elements + return elements.filter((element) => filterSearch(searchString, element)) +} + +function filterElementsByUriPrefix(elements, searchUriPrefix) { + if (!searchUriPrefix) return elements + return elements.filter((element) => filterUriPrefix(searchUriPrefix, element)) +} + +const useFilteredElements = (elements, selectFilterChanged, selectedUriPrefix, searchString) => { + return useMemo(() => { + let filteredElements = filterElementsByChanged(elements, selectFilterChanged) + filteredElements = filterElementsByUriPrefix(filteredElements, selectedUriPrefix) + filteredElements = filterElementsByUri(filteredElements, searchString) + return filteredElements + }, [elements, selectFilterChanged, selectedUriPrefix, searchString]) +} + +export default useFilteredElements diff --git a/rdmo/management/assets/js/utils/processElementDiffs.js b/rdmo/management/assets/js/utils/processElementDiffs.js new file mode 100644 index 0000000000..4c8856ef27 --- /dev/null +++ b/rdmo/management/assets/js/utils/processElementDiffs.js @@ -0,0 +1,33 @@ +// utils/processElementDiffs.js +import getDiff from './getDiff' // Make sure the path is correct + +function processElementDiffs(element) { + let changedElement = false + let changedFields = [] + + const updatedAndChanged = element.updated_and_changed + + // Iterate over each field that might have changed + const updatedWithDiffs = Object.entries(updatedAndChanged).reduce((acc, [key, { current_data, new_data }]) => { + const elementFieldDiff = getDiff(current_data, new_data) + + // Determine if the field has changed + if (elementFieldDiff.changed) { + changedFields.push(key) + changedElement = true + } + + // Update the accumulator with new diff data + acc[key] = elementFieldDiff + return acc + }, {}) + + return { + ...element, + updated_and_changed: updatedWithDiffs, + changedFields: changedFields, + changed: changedElement + } +} + +export default processElementDiffs diff --git a/rdmo/management/assets/scss/management.scss b/rdmo/management/assets/scss/management.scss index 580107c1d2..dae39839e4 100644 --- a/rdmo/management/assets/scss/management.scss +++ b/rdmo/management/assets/scss/management.scss @@ -62,6 +62,32 @@ $icon-font-path: "bootstrap-sass/assets/fonts/bootstrap/"; font-weight: normal; } } + .horizontal-container { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 15px 0px; + + .shown-info { + margin-left: auto; + } + } + } + + &.panel-import-warnings, + &.panel-import-errors { + cursor: pointer; + } + + &.panel-import-warnings { + .element-link { + color: $warning-color; + } + } + &.panel-import-errors { + .element-link { + color: $danger-color; + } } } .pull-right { @@ -116,6 +142,11 @@ $icon-font-path: "bootstrap-sass/assets/fonts/bootstrap/"; .drag { color: $link-color; + + &:hover { + color: $link-color-hover; + } + cursor: grab; } @@ -203,3 +234,62 @@ $icon-font-path: "bootstrap-sass/assets/fonts/bootstrap/"; flex: 1; } } + +.field-diff { + table[class*="react-diff-"][class*="diff-container"] { + margin: 0; + border-collapse: separate; // needed for border radius since this is a table + + tr { + td[class*="marker"] { + border-left: 1px solid #ccc; + } + td[class*="content"] { + border-right: 1px solid #ccc; + } + &:first-child { + td[class*="marker"] { + border-top: 1px solid #ccc; + border-left: 1px solid #ccc; + border-top-left-radius: 4px; + } + td[class*="content"] { + border-top: 1px solid #ccc; + border-right: 1px solid #ccc; + border-top-right-radius: 4px; + } + } + &:last-child { + td[class*="marker"] { + border-bottom: 1px solid #ccc; + border-left: 1px solid #ccc; + border-bottom-left-radius: 4px; + } + td[class*="content"] { + border-bottom: 1px solid #ccc; + border-right: 1px solid #ccc; + border-bottom-right-radius: 4px; + } + } + } + + // Target elements with classes containing "react-diff-", and "marker" + // Target elements with classes containing "react-diff-", and "content" + [class*="marker"], + [class*="content"] { + padding: 8px 12px; + vertical-align: top; + pre { + padding: 5px 10px; + border-radius: 4px; + font-size: 13px; + line-height: 18px; + background-color: #fff; + } + } + + [class*="marker"] { + padding-right: 0; + } + } +} diff --git a/rdmo/management/constants.py b/rdmo/management/constants.py new file mode 100644 index 0000000000..4c1ddc78a9 --- /dev/null +++ b/rdmo/management/constants.py @@ -0,0 +1,21 @@ + +from rdmo.conditions.models import Condition +from rdmo.domain.models import Attribute +from rdmo.options.models import Option, OptionSet +from rdmo.questions.models import Catalog, Page, Question, QuestionSet, Section +from rdmo.tasks.models import Task +from rdmo.views.models import View + +RDMO_MODEL_PATH_MAPPER = { + 'conditions.condition': Condition, + 'domain.attribute': Attribute, + 'options.optionset': OptionSet, + 'options.option': Option, + 'questions.catalog': Catalog, + 'questions.section': Section, + 'questions.page': Page, + 'questions.questionset': QuestionSet, + 'questions.question': Question, + 'tasks.task': Task, + 'views.view': View + } diff --git a/rdmo/management/import_utils.py b/rdmo/management/import_utils.py new file mode 100644 index 0000000000..5ec31d3350 --- /dev/null +++ b/rdmo/management/import_utils.py @@ -0,0 +1,163 @@ +import logging +from collections import defaultdict +from dataclasses import asdict +from typing import Dict, Set, Tuple + +from django.core.exceptions import FieldDoesNotExist +from django.db.models import Model + +from rest_framework.exceptions import ValidationError +from rest_framework.serializers import ModelSerializer + +from rdmo.core.imports import ( + ImportElementFields, + set_common_fields, + set_extra_field, + set_foreign_field, + set_lang_field, + set_m2m_instances, + set_m2m_through_instances, + set_reverse_m2m_through_instance, + track_changes_on_element, +) + +logger = logging.getLogger(__name__) + +IMPORT_ELEMENT_INIT_DICT = { + ImportElementFields.WARNINGS: lambda: defaultdict(list), + ImportElementFields.ERRORS: list, + ImportElementFields.CREATED: bool, + ImportElementFields.UPDATED: bool, + ImportElementFields.DIFF: dict, + } + + +def is_valid_import_element(element: dict) -> bool: + if element is None or not isinstance(element, dict): + return False + if not all(i in element for i in ['model', 'uri']): + return False + return True + + +def get_redundant_keys_from_element(element_keys: Set, model: Model) -> Set: + model_fields = {i.name for i in model._meta.get_fields()} + required_element_keys = {'uri', 'model'} + import_dict_keys = {i.value for i in IMPORT_ELEMENT_INIT_DICT.keys()} + redundant_keys = element_keys - model_fields - required_element_keys - import_dict_keys + + lang_fields_prefix = {i.split('_lang')[0] for i in model_fields if 'lang' in i} + element_lang_keys = {i for i in element_keys if any(i.startswith(a) for a in lang_fields_prefix)} + redundant_keys = redundant_keys - element_lang_keys + return redundant_keys + +def initialize_import_element_dict(element: Dict) -> None: + # initialize element dict with default values + for _k,_val in IMPORT_ELEMENT_INIT_DICT.items(): + element[_k] = _val() + return element + + +def initialize_and_clean_import_element_dict(element: Dict, model: Model) -> Tuple[Dict, Dict]: + redundant_keys = get_redundant_keys_from_element(set(element.keys()), model) + excluded_element_data = {} + for k in redundant_keys: + excluded_element_data[k] = element.pop(k) + # initialize element dict with default values + element = initialize_import_element_dict(element) + return element, excluded_element_data + + +def strip_uri_prefix_endswith_slash(element: dict) -> dict: + """Removes the trailing slash from the URI prefix if it exists.""" + if 'uri_prefix' in element and element['uri_prefix'].endswith('/'): + element['uri_prefix'] = element['uri_prefix'].rstrip('/') + return element + + +def apply_field_values(instance, element, import_helper, original) -> None: + """Applies the field values from the element to the instance.""" + # start to set values on the instance + # set common field values from element on instance + for field in import_helper.common_fields: + set_common_fields(instance, field, element, original=original) + # set language fields + for field in import_helper.lang_fields: + set_lang_field(instance, field, element, original=original) + # set foreign fields + for field in import_helper.foreign_fields: + set_foreign_field(instance, field, element, original=original) + # set extra fields, track changes is done after instance.full_clean + for extra_field in import_helper.extra_fields: + set_extra_field(instance, extra_field.field_name, element, + extra_field_helper=extra_field) + + +def validate_with_serializer_field(instance, field_name, value): + """Validate and convert a value using the corresponding DRF serializer field.""" + + # Ensure the field exists on the model + try: + model_field = instance._meta.get_field(field_name) + except FieldDoesNotExist: + logger.debug("Field '%s' does not exist on the model.", field_name) + return None + + # Use ModelSerializer's field building logic + serializer = ModelSerializer(instance=instance) + try: + field_class, field_kwargs = serializer.build_standard_field(field_name, model_field) + except (KeyError, AttributeError): + logger.info("Could not build a field for '%s'.", field_name) + return None + + # Handle None values and null fields + if value is None and field_kwargs.get('allow_null', False): + return value # None is allowed, no need to validate further + + try: + # Instantiate the field with the kwargs and run validation + drf_field = field_class(**field_kwargs) + return drf_field.run_validation(value) + except ValidationError as e: + # Log only if the value is truly invalid + if value is not None: + logger.info("Cannot convert '%s' for field '%s' using '%s': %s", + value, field_name, field_class.__name__, str(e)) + return None + + +def update_extra_fields_from_validated_instance(instance, element, import_helper, original=None) -> None: + + for extra_field in import_helper.extra_fields: + field_name = extra_field.field_name + + element_field_value = element.get(field_name) + + # deserialize the element value by using the drf field serializer + validated_value = validate_with_serializer_field(instance, field_name, element_field_value) + + if validated_value is not None: + element[field_name] = validated_value + + # track changes + track_changes_on_element(element, field_name, new_value=element[field_name], original=original) + + +def update_related_fields(instance, element, import_helper, original, save) -> None: + # this part updates the related fields of the instance + for m2m_field in import_helper.m2m_instance_fields: + set_m2m_instances(instance, element, m2m_field, original=original, save=save) + for m2m_through_fields in import_helper.m2m_through_instance_fields: + set_m2m_through_instances(instance, element, **asdict(m2m_through_fields), + original=original, save=save) + for reverse_m2m_fields in import_helper.reverse_m2m_through_instance_fields: + set_reverse_m2m_through_instance(instance, element, **asdict(reverse_m2m_fields), + original=original, save=save) + + +def add_current_site_to_sites_and_editor(instance, current_site, import_helper): + if import_helper.add_current_site_editors: + instance.editors.add(current_site) + if import_helper.add_current_site_sites: + instance.sites.add(current_site) diff --git a/rdmo/management/imports.py b/rdmo/management/imports.py index e47a3c7001..3e483d24aa 100644 --- a/rdmo/management/imports.py +++ b/rdmo/management/imports.py @@ -1,66 +1,162 @@ -from collections import defaultdict +import copy +import logging +from collections import OrderedDict +from typing import Dict, List, Optional + +from django.conf import settings +from django.contrib.sites.shortcuts import get_current_site +from django.http import HttpRequest + +from rdmo.conditions.imports import import_helper_condition +from rdmo.core.imports import ( + ImportElementFields, + check_permissions, + get_or_return_instance, + make_import_info_msg, + validate_instance, +) +from rdmo.core.xml import order_elements +from rdmo.domain.imports import import_helper_attribute +from rdmo.management.import_utils import ( + add_current_site_to_sites_and_editor, + apply_field_values, + initialize_and_clean_import_element_dict, + initialize_import_element_dict, + is_valid_import_element, + strip_uri_prefix_endswith_slash, + update_extra_fields_from_validated_instance, + update_related_fields, +) +from rdmo.options.imports import import_helper_option, import_helper_optionset +from rdmo.questions.imports import ( + import_helper_catalog, + import_helper_page, + import_helper_question, + import_helper_questionset, + import_helper_section, +) +from rdmo.tasks.imports import import_helper_task +from rdmo.views.imports import import_helper_view + +logger = logging.getLogger(__name__) + +ELEMENT_IMPORT_HELPERS = { + "domain.attribute": import_helper_attribute, + "options.option": import_helper_option, + "conditions.condition": import_helper_condition, + "options.optionset": import_helper_optionset, + "questions.question": import_helper_question, + "questions.questionset": import_helper_questionset, + "questions.section": import_helper_section, + "questions.page": import_helper_page, + "questions.catalog": import_helper_catalog, + "tasks.task": import_helper_task, + "views.view": import_helper_view +} + + +def import_elements(uploaded_elements: OrderedDict, + save: bool = True, + request: Optional[HttpRequest] = None) -> List[Dict]: + imported_elements = [] + uploaded_elements_initial_ordering = {uri: n for n, uri in enumerate(uploaded_elements.keys())} + uploaded_uris = set(uploaded_elements.keys()) + current_site = get_current_site(request) + if save: + # when saving, the elements are ordered according to the rdmo models + pass + uploaded_elements = order_elements(uploaded_elements) + + for _uri, uploaded_element in uploaded_elements.items(): + if not is_valid_import_element(uploaded_element): + continue + element = import_element( + element=uploaded_element, + save=save, + request=request, + current_site=current_site + ) + element[ImportElementFields.WARNINGS] = { + k: val for + k, val in element[ImportElementFields.WARNINGS].items() + if k not in uploaded_uris + } + imported_elements.append(element) + + # sort elements back to initial order of uploaded elements + imported_elements = sorted( + imported_elements, + key=lambda x: uploaded_elements_initial_ordering.get(x['uri'], + float('inf')) + ) + + return imported_elements + + +def import_element( + element: Optional[Dict] = None, + save: bool = True, + request: Optional[HttpRequest] = None, + current_site = None + ) -> Dict: + + initialize_import_element_dict(element) + + import_helper = ELEMENT_IMPORT_HELPERS[element['model']] + + uri = element.get('uri') + + element, _excluded_data = initialize_and_clean_import_element_dict(element, import_helper.model) + + # get or create instance from uri and model + instance, created = get_or_return_instance(import_helper.model, uri=uri) + + # keep a copy of the original + # when the element is updated + # needs to be created here, else the changes will be overwritten + original = copy.deepcopy(instance) if not created else None + + # prepare a log message + msg = make_import_info_msg(import_helper.model._meta.verbose_name, created, uri=uri) + + # check the change or add permissions for the user on the instance + user = request.user if request is not None else None + perms_error_msg = check_permissions(instance, uri, user) + if perms_error_msg: + # when there is an error msg, the import can be stopped and return + element[ImportElementFields.ERRORS].append(perms_error_msg) + return element + + element[ImportElementFields.CREATED] = created + element[ImportElementFields.UPDATED] = not created and original is not None + # INFO: the dict element[FieldNames.diff.value] is filled by calling track_changes_on_element + + element = strip_uri_prefix_endswith_slash(element) + # start to set values on the instance + apply_field_values(instance, element, import_helper, original) + + # call the validators on the instance + validate_instance(instance, element, *import_helper.validators) + + update_extra_fields_from_validated_instance(instance, element, import_helper, original=original) + + if element.get(ImportElementFields.ERRORS): + # when there is an error msg, the import can be stopped and return + if save: + element[ImportElementFields.CREATED] = False + element[ImportElementFields.UPDATED] = False + return element + + if save: + logger.info(msg) + instance.save() + + update_related_fields(instance, element, import_helper, original, save) + + if created and settings.MULTISITE: + add_current_site_to_sites_and_editor(instance, current_site, import_helper) + + elif not created: # when an element will be updated but not saved + update_related_fields(instance, element, import_helper, original, save) -from rdmo.conditions.imports import import_condition -from rdmo.domain.imports import import_attribute -from rdmo.options.imports import import_option, import_optionset -from rdmo.questions.imports import import_catalog, import_page, import_question, import_questionset, import_section -from rdmo.tasks.imports import import_task -from rdmo.views.imports import import_view - - -def import_elements(elements, save=True, user=None): - for element in elements: - model = element.get('model') - - element.update({ - 'warnings': defaultdict(list), - 'errors': [], - 'created': False, - 'updated': False - }) - - if model == 'conditions.condition': - import_condition(element, save, user) - - elif model == 'domain.attribute': - import_attribute(element, save, user) - - elif model == 'options.optionset': - import_optionset(element, save, user) - - elif model == 'options.option': - import_option(element, save, user) - - elif model == 'questions.catalog': - import_catalog(element, save, user) - - elif model == 'questions.section': - import_section(element, save, user) - - elif model == 'questions.page': - import_page(element, save, user) - - elif model == 'questions.questionset': - import_questionset(element, save, user) - - elif model == 'questions.question': - import_question(element, save, user) - - elif model == 'tasks.task': - import_task(element, save, user) - - elif model == 'views.view': - import_view(element, save, user) - - element = filter_warnings(element, elements) - - -def filter_warnings(element, elements): - # remove warnings regarding elements which are in the elements list - warnings = [] - for uri, messages in element['warnings'].items(): - if not next(filter(lambda e: e['uri'] == uri, elements), None): - warnings += messages - - element['warnings'] = warnings return element diff --git a/rdmo/management/management/commands/import.py b/rdmo/management/management/commands/import.py index 3438164220..e145118ef3 100644 --- a/rdmo/management/management/commands/import.py +++ b/rdmo/management/management/commands/import.py @@ -1,9 +1,8 @@ import logging from django.core.management.base import BaseCommand, CommandError -from django.utils.translation import gettext_lazy as _ -from rdmo.core.xml import convert_elements, flat_xml_to_elements, order_elements, read_xml_file +from rdmo.core.xml import parse_xml_to_elements from rdmo.management.imports import import_elements logger = logging.getLogger(__name__) @@ -15,16 +14,16 @@ def add_arguments(self, parser): parser.add_argument('xmlfile', action='store', default=False, help='RDMO XML export file') def handle(self, *args, **options): - root = read_xml_file(options['xmlfile']) - if root is None: - raise CommandError(_('The content of the xml file does not consist of well formed data or markup.')) - elif root.tag != 'rdmo': - raise CommandError(_('This XML does not contain RDMO content.')) - else: - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - - import_elements(elements) + + try: + xml_parsed_elements, errors = parse_xml_to_elements(xml_file=options['xmlfile']) + except CommandError as e: + logger.info('Import failed with XML parsing errors.') + raise CommandError(str(e)) from e + + # raise exception when xml parsing returned any errors + if errors: + logger.info('Import failed with XML validation errors.') + raise CommandError(" ".join(map(str, errors))) + + import_elements(xml_parsed_elements) diff --git a/rdmo/management/management/commands/merge_attributes.py b/rdmo/management/management/commands/merge_attributes.py new file mode 100644 index 0000000000..a1550b4d22 --- /dev/null +++ b/rdmo/management/management/commands/merge_attributes.py @@ -0,0 +1,261 @@ +import logging +from textwrap import dedent + +from django.core.management.base import BaseCommand, CommandError + +from rdmo.domain.models import Attribute +from rdmo.management.utils import replace_uri_in_template_string +from rdmo.projects.models import Value +from rdmo.views.models import View + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = 'Replace an attribute with another attribute across multiple models' + + def add_arguments(self, parser): + parser.add_argument( + '--source', + required=True, + help='The URI of the source attribute that will be replaced by the target and will be deleted' + ) + parser.add_argument( + '--target', + required=True, + help='The URI of the target attribute that will be used to replace the source' + ) + parser.add_argument( + '--save', + action='store_true', + default=False, + help='''If specified, the changes will be saved.\ + If not specified, the command will not make any changes in the database.''' + ) + parser.add_argument( + '--delete', + action='store_true', + default=False, + help='''If specified, the source attribute will be deleted.\ + If not specified, the command will not delete the source attribute.''' + ) + parser.add_argument( + '--view', + action='store_true', + default=False, + help='If specified, the changes will be applied to the affected Views as well.' + ) + + def handle(self, *args, **options): + source = options['source'] + target = options['target'] + save_changes = options['save'] + delete_source = options['delete'] + update_views = options['view'] + verbosity = options.get('verbosity', 1) + + source = get_valid_attribute(source, message_name='Source') + target = get_valid_attribute(target, message_name='Target', allow_descendants=True) + + if source == target: + raise CommandError("Source and Target attribute are the same, nothing to do.") + + results = {} + related_model_fields = [i for i in Attribute._meta.get_fields() + if i.is_relation and not i.many_to_many + and i.related_model is not Attribute] + + for related_field in related_model_fields: + replaced_model_result = replace_attribute_on_related_model_instances(related_field, source=source, + target=target, + save_changes=save_changes) + results[related_field.related_model._meta.verbose_name_raw] = replaced_model_result + + view_template_result = replace_attribute_in_view_template(source=source, target=target, + save_changes=save_changes, update_views=update_views) + results[View._meta.verbose_name_raw] = view_template_result + + if delete_source: + try: + source.delete() + logger.info("Source attribute %s was deleted.", source.uri) + except source.DoesNotExist: + logger.info("Source attribute %s did not exist.", source.uri) + + if verbosity >= 1: + all_instances_were_updated = all(a['saved'] for i in results.values() for a in i) + affect_elements_msg = make_affected_elements_counts_message(results) + affected_projects_msg = make_affected_projects_message(results) + affected_views_msg = make_affected_views_message(results) + if save_changes: + info_msg = f"Successfully replaced source attribute {source.uri} with {target.uri} on affected elements." # noqa: E501 + if update_views: + info_msg += f"\nSuccessfully replaced source attribute {source.uri} in affected View templates." + self.stdout.write(self.style.SUCCESS(info_msg)) + + if delete_source: + self.stdout.write(self.style.SUCCESS( + f"Source attribute {source.uri} was deleted.\n" + )) + if not all_instances_were_updated: + self.stdout.write( + self.style.NOTICE(f"Source attribute {source.uri} was deleted without moving affected elements to the target attribute.") # noqa: E501 + ) + if not save_changes and not delete_source: + self.stdout.write(self.style.SUCCESS( + f"Nothing was changed. Displaying affected elements for replacement of source attribute {source.uri} with target {target.uri}." # noqa: E501 + )) + + self.stdout.write(self.style.SUCCESS( + f"{affect_elements_msg}\n" + f"{affected_projects_msg}\n" + f"{affected_views_msg}" + )) + + if verbosity >= 2: + affected_instances_msg = make_affected_instances_detail_message(results) + self.stdout.write() + self.stdout.write(affected_instances_msg) + + +def get_attribute_from_uri(attribute_uri, message_name=''): + try: + attribute = Attribute.objects.get(uri=attribute_uri) + return attribute + except Attribute.DoesNotExist as e: + raise CommandError(f"{message_name} attribute {attribute_uri} does not exist.") from e + except Attribute.MultipleObjectsReturned as e: + raise CommandError(f"{message_name} attribute {attribute_uri} returns multiple objects.") from e + + +def validate_attribute(attribute, message_name='', allow_descendants=None): + if not isinstance(attribute, Attribute): + raise CommandError(f"{message_name} attribute argument should be of type Attribute.") + + if not allow_descendants and attribute.get_descendants().exists(): + raise CommandError(f"{message_name} attribute '{attribute.uri}' with descendants is not supported.") + + +def get_valid_attribute(attribute, message_name='', allow_descendants=None): + if isinstance(attribute, str): + attribute = get_attribute_from_uri(attribute, message_name=message_name) + + validate_attribute(attribute, message_name=message_name, allow_descendants=allow_descendants) + + return attribute + + +def replace_attribute_on_related_model_instances(related_field, source=None, target=None, save_changes=False): + model = related_field.related_model + lookup_field = related_field.remote_field.name + qs = model.objects.filter(**{lookup_field: source}) + + replacement_results = [] + for instance in qs: + instance_source = getattr(instance, lookup_field) + + if isinstance(instance_source, Attribute) and instance_source != source: + raise CommandError("Instance attribute should be equal to the source attribute") + + setattr(instance, lookup_field, target) + + if save_changes: + instance.save() + logger.info( + "Attribute %s on %s(id=%s) was replaced with %s.", + source.uri, model._meta.verbose_name_raw, instance.id, target.uri + ) + + replacement_results.append({ + 'model_name': model._meta.verbose_name_raw, + 'instance': instance, + 'source': source, + 'target': target, + 'saved': save_changes, + }) + return replacement_results + + +def replace_attribute_in_view_template(source=None, target=None, save_changes=False, update_views=False): + qs = View.objects.filter(**{"template__contains": source.path}) + replacement_results = [] + for instance in qs: + + template_target = replace_uri_in_template_string(instance.template, source.uri, target.uri) + instance.template = template_target + + if save_changes and update_views: + instance.save() + logger.info( + "Attribute %s in %s(id=%s) template was replaced with target %s.", + source.uri, View._meta.verbose_name_raw, instance.id, target.uri + ) + + replacement_results.append({ + 'model_name': View._meta.verbose_name_raw, + 'instance': instance, + 'source': source, + 'target': target, + 'saved': save_changes, + }) + return replacement_results + + +def get_affected_projects_from_results(results): + value_results = results.get(Value._meta.verbose_name_raw, []) + return list({i['instance'].project for i in value_results}) + + +def make_affected_elements_counts_message(results): + element_counts = ", ".join([f"{k.capitalize()}({len(v)})" for k, v in results.items() if v]) + if not element_counts or not any(results.values()): + return "No elements affected." + affected_projects = get_affected_projects_from_results(results) + if affected_projects: + element_counts += f", Project({len(affected_projects)})" + return f"Affected elements: {element_counts}" + + +def make_affected_projects_message(results): + affected_projects = get_affected_projects_from_results(results) + if not affected_projects: + return "No Projects are affected." + msg = "Affected Projects:" + for project in affected_projects: + msg += f"\n\t{project.id}\t{project}" + return msg + + +def make_affected_views_message(results): + view_results = results.get(View._meta.verbose_name_raw, []) + if not view_results: + return "No Views affected." + msg = "Affected Views:" + for view in {i['instance'] for i in view_results}: + msg += f"\n\t{view.id}\t{view}" + return msg + + +def make_affected_instances_detail_message(results): + if not results: + return "" + msg = "" + for k, merger_results in results.items(): + if not merger_results: + continue + msg += f"Merge attribute results for model {k.capitalize()} ({len(merger_results)})" + msg += '\n' + for result in merger_results: + msg += dedent(f'''\ + + {result['model_name']}(id={result['instance'].id}) + instance={result['instance']} + source={result['source'].uri} + target={result['target'].uri} + saved={result['saved']} + ''') + if hasattr(result['instance'], 'project'): + msg += f"Project(id={result['instance'].project.id}) {result['instance'].project}" + msg += '\n' + msg += '\n' + return msg diff --git a/rdmo/management/rules.py b/rdmo/management/rules.py index 3014306cfe..7d71fc2420 100644 --- a/rdmo/management/rules.py +++ b/rdmo/management/rules.py @@ -1,10 +1,8 @@ -import logging +from django.conf import settings import rules from rules.predicates import is_authenticated, is_superuser -logger = logging.getLogger(__name__) - @rules.predicate def is_editor(user) -> bool: @@ -13,16 +11,18 @@ def is_editor(user) -> bool: @rules.predicate -def is_editor_for_current_site(user, site) -> bool: +def is_editor_for_current_site(user) -> bool: ''' Checks if any editor role exists for the user ''' if not is_editor(user): return False # if the user is not an editor, return False - return user.role.editor.filter(pk=site.pk).exists() + return user.role.editor.filter(id=settings.SITE_ID).exists() @rules.predicate def is_element_editor(user, obj) -> bool: ''' Checks if the user is an editor for the sites to which this element is editable ''' + if obj is None: + return False if obj.id is None: # for _add_object permissions # if the element does not exist yet, it can be created by all users with an editor role @@ -43,16 +43,18 @@ def is_reviewer(user) -> bool: @rules.predicate -def is_reviewer_for_current_site(user, site) -> bool: +def is_reviewer_for_current_site(user) -> bool: ''' Checks if any reviewer role exists for the user ''' if not is_reviewer(user): return False # if the user is not an reviewer, return False - return user.role.reviewer.filter(pk=site.pk).exists() + return user.role.reviewer.filter(id=settings.SITE_ID).exists() @rules.predicate def is_element_reviewer(user, obj) -> bool: ''' Checks if the user is an reviewer for the sites to which this element is editable ''' + if obj is None: + return False # if the element has no editors, it is reviewable by all reviewers if not obj.editors.exists(): @@ -144,6 +146,8 @@ def is_legacy_reviewer(user) -> bool: rules.add_perm('tasks.add_task_object', is_element_editor) rules.add_perm('tasks.change_task_object', is_element_editor) rules.add_perm('tasks.delete_task_object', is_element_editor) +# toggle current site field perm +rules.add_perm('tasks.change_task_toggle_site', is_editor_for_current_site) # Model Permissions for views rules.add_perm('views.view_view', is_editor | is_reviewer) @@ -154,6 +158,8 @@ def is_legacy_reviewer(user) -> bool: rules.add_perm('views.add_view_object', is_element_editor) rules.add_perm('views.change_view_object', is_element_editor) rules.add_perm('views.delete_view_object', is_element_editor) +# toggle current site field perm +rules.add_perm('views.change_view_toggle_site', is_editor_for_current_site) # Model permissions for catalogs rules.add_perm('questions.view_catalog', is_editor | is_reviewer) @@ -164,6 +170,8 @@ def is_legacy_reviewer(user) -> bool: rules.add_perm('questions.add_catalog_object', is_element_editor) rules.add_perm('questions.change_catalog_object', is_element_editor) rules.add_perm('questions.delete_catalog_object', is_element_editor) +# toggle current site field perm +rules.add_perm('questions.change_catalog_toggle_site', is_editor_for_current_site) # Model permissions for sections rules.add_perm('questions.view_section', is_editor | is_reviewer) diff --git a/rdmo/management/tests/__init__.py b/rdmo/management/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rdmo/management/tests/e2e/__init__.py b/rdmo/management/tests/e2e/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rdmo/management/tests/e2e/conftest.py b/rdmo/management/tests/e2e/conftest.py new file mode 100644 index 0000000000..44d1321a2f --- /dev/null +++ b/rdmo/management/tests/e2e/conftest.py @@ -0,0 +1,72 @@ +import os + +import pytest + +from django.conf import settings +from django.contrib.auth import get_user_model +from django.core.management import call_command +from django.test import Client + +from playwright.sync_api import Page + +from rdmo.accounts.utils import set_group_permissions + +USERNAME = "editor" # the user needs exist in the database +PLAYWRIGHT_TIMEOUT = 10_000 # timeout in ms + + +@pytest.fixture(scope="session", autouse=True) +def _set_django_allow_async_unsafe(): + """pytest-playwright needs this setting to be enabled.""" + os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "true") + + +@pytest.fixture +def django_db_setup(django_db_setup, django_db_blocker, fixtures): # noqa: PT004 - pytest-django requires this name "django_db_setup" + """Set up database and populate with fixtures, that get restored for every test case. + + This fixture overrides the django_db_setup in the main conftest.py, this only applies to the e2e tests + in this directory. + """ + with django_db_blocker.unblock(): + call_command("loaddata", *fixtures, verbosity=0) + set_group_permissions() + + +@pytest.fixture +def authenticated_client(db) -> Client: + """An authenticated test client, used to bypass the login page.""" + user = get_user_model().objects.get(username=USERNAME) + client = Client() + client.user = user # attach user to client to access in other fixtures + client.force_login(user) + return client + + +@pytest.fixture +def page(live_server, browser, authenticated_client) -> Page: + """An authenticated playwright page. + + The page is authenticated with session cookies from authenticated_client. + The page has access to the live server and starts at "/management". + The page has the authenticated user attached to it. + """ + + # retrieve the session cookie from the authenticated client + session_cookie = authenticated_client.cookies[settings.SESSION_COOKIE_NAME] + cookie = { + "name": session_cookie.key, + "value": session_cookie.value, + "url": live_server.url, + } + context = browser.new_context(base_url=live_server.url) + # the browser context is now "authenticated" with the session cookie + context.add_cookies([cookie]) + page = context.new_page() + page.set_default_timeout(PLAYWRIGHT_TIMEOUT) + page.set_default_navigation_timeout(PLAYWRIGHT_TIMEOUT) + page.user = authenticated_client.user # attach user to page to access in tests + # the page starts at base_url + /management + page.goto("/management") + yield page + context.close() diff --git a/rdmo/management/tests/e2e/test_frontend_import_options.py b/rdmo/management/tests/e2e/test_frontend_import_options.py new file mode 100644 index 0000000000..b3f2dab3ba --- /dev/null +++ b/rdmo/management/tests/e2e/test_frontend_import_options.py @@ -0,0 +1,75 @@ +# ruff: noqa: F811 +import pytest + +from playwright.sync_api import Page, expect + +from rdmo.management.tests.helpers_import_elements import IMPORT_ELEMENT_PANELS_LOCATOR +from rdmo.management.tests.helpers_models import delete_all_objects +from rdmo.options.models import Option, OptionSet + +pytestmark = pytest.mark.e2e + +import_xml = "./testing/xml/elements/optionsets.xml" +import_xml_1 = "./testing/xml/elements/updated-and-changed/optionsets-1.xml" +OPTIONSETS_COUNTS = {"total": 13, "updated": 13, "changed": 5, "warnings": 2} +OPTIONSETS_COUNTS_HEADER_INFOS = [f"{k.capitalize()}: {v}" for k, v in OPTIONSETS_COUNTS.items()] +# Defined in filterCheckBoxText in rdmo/management/assets/js/components/import/common/ImportFilters.js +IMPORT_FILTER_LABEL_TEXT = 'Show only new and changed elements (%s)' + + +def test_import_and_update_optionsets_in_management(page: Page) -> None: + """Test that each content type is available through the navigation.""" + delete_all_objects([OptionSet, Option]) + + expect(page.get_by_role("heading", name="Management")).to_be_visible() + expect(page.locator("strong").filter(has_text="Catalogs")).to_be_visible() + ## 1. Import fresh optionset.xml + # choose the file to be imported + page.locator('input[name="uploaded_file"]').set_input_files(import_xml) + # click the import form submit button, this will take some time + page.locator( + "#sidebar div.elements-sidebar form.upload-form.sidebar-form div.sidebar-form-button button.btn.btn-primary" + ).click() + # wait for import to be finished with timeout 30s + expect(page.get_by_text("Import from: optionsets.xml")).to_be_visible(timeout=30_000) + ## TODO test if ImportInfo numbers are correct + # test the components of the import-before-import staging page + expect(page.get_by_text(f"Created: {OPTIONSETS_COUNTS['total']}")).to_be_visible(timeout=30_000) + page.locator(".element-link").first.click() + page.get_by_role("link", name="Deselect all").click() + page.get_by_role("link", name="Select all", exact=True).click() + page.get_by_role("link", name="Show all", exact=True).click() + rows_displayed_in_ui = page.locator(IMPORT_ELEMENT_PANELS_LOCATOR) + expect(rows_displayed_in_ui).to_have_count(OPTIONSETS_COUNTS["total"]) + # click the import button to start saving the instances to the db + page.get_by_role("button", name=f"Import {OPTIONSETS_COUNTS['total']} elements").click() + expect(page.get_by_role("heading", name="Import successful")).to_be_visible() + page.screenshot(path="screenshots/management-import-optionsets-post-import.png", full_page=True) + page.get_by_text("Created:").click() + # go back to management page + page.get_by_role("button", name="Back").click() + expect(page.get_by_role("heading", name="Management")).to_be_visible() + # assert all Model objects in db + assert OptionSet.objects.count() == 4 + assert Option.objects.count() == 9 + + ## 2. import optionset-1.xml with changes + # choose the file to be imported + page.locator('input[name="uploaded_file"]').set_input_files(import_xml_1) + # click the import form submit button, this will take some time + page.locator( + "#sidebar div.elements-sidebar form.upload-form.sidebar-form div.sidebar-form-button button.btn.btn-primary" + ).click() + expect(page.get_by_text("Import from: optionsets-1.xml")).to_be_visible(timeout=40_000) + # assert changed elements + for text in OPTIONSETS_COUNTS_HEADER_INFOS: + expect(page.locator("#main")).to_contain_text(text) + expect(page.get_by_text(IMPORT_FILTER_LABEL_TEXT % OPTIONSETS_COUNTS['changed'])).to_be_visible() + page.get_by_role("link", name="Show changes").click() + expect(page.locator(".col-sm-6 > .form-group").first).to_be_visible(timeout=30_000) + # take a screenshot of the import page + expect(page.get_by_text("http://example.com/terms/options/one_two_three/three").nth(1)).to_be_visible() + page.locator("body").press("Home") + expect(page.get_by_role("link", name="Management", exact=True)).to_be_visible() + page.screenshot(path="screenshots/management-import-optionsets-1-changes.png", full_page=True) + ## TODO test for warnings, errors diff --git a/rdmo/management/tests/e2e/test_frontend_import_questions.py b/rdmo/management/tests/e2e/test_frontend_import_questions.py new file mode 100644 index 0000000000..3de152050c --- /dev/null +++ b/rdmo/management/tests/e2e/test_frontend_import_questions.py @@ -0,0 +1,54 @@ +# ruff: noqa: F811 +import pytest + +from playwright.sync_api import expect + +from rdmo.management.tests.helpers_import_elements import IMPORT_ELEMENT_PANELS_LOCATOR_SHOWN +from rdmo.management.tests.helpers_models import delete_all_objects +from rdmo.questions.models import Catalog, Question, Section +from rdmo.questions.models import Page as PageModel +from rdmo.questions.models.questionset import QuestionSet + +pytestmark = pytest.mark.e2e + + +def test_import_catalogs_in_management(page) -> None: + """Test that the catalogs.xml can be imported correctly.""" + + delete_all_objects([Catalog, Section, PageModel, QuestionSet, Question]) + + expect(page.get_by_role("heading", name="Management")).to_be_visible() + expect(page.locator("strong").filter(has_text="Catalogs")).to_be_visible() + # choose the file to be imported + page.locator('input[name="uploaded_file"]').set_input_files("./testing/xml/elements/catalogs.xml") + # click the import form submit button, this will take some time + page.locator( + "#sidebar div.elements-sidebar form.upload-form.sidebar-form div.sidebar-form-button button.btn.btn-primary" + ).click() + # wait for import to be finished with timeout 30s + expect(page.get_by_text("Import from: catalogs.xml")).to_be_visible(timeout=30_000) + ## TODO test if ImportInfo numbers are correct + # test the components of the import-before-import staging page + page.locator(".element-link").first.click() + page.get_by_role("link", name="Deselect all").click() + page.get_by_role("link", name="Select all", exact=True).click() + page.get_by_role("link", name="Show all").click() + rows_displayed_in_ui_show = page.locator(IMPORT_ELEMENT_PANELS_LOCATOR_SHOWN).get_by_text("URI prefix", exact=True) + expect(rows_displayed_in_ui_show).to_have_count(148) + page.get_by_role("link", name="Hide all").click() + expect(rows_displayed_in_ui_show).to_have_count(0) + page.screenshot(path="screenshots/management-import-catalogs-pre.png", full_page=True) + # click the import button to start saving the instances to the db + page.get_by_role("button", name="Import 148 elements").click() + expect(page.get_by_role("heading", name="Import successful")).to_be_visible() + page.screenshot(path="screenshots/management-import-catalogs-post.png", full_page=True) + page.get_by_text("Created:").click() + # go back to management page + page.get_by_role("button", name="Back").click() + expect(page.get_by_role("heading", name="Management")).to_be_visible() + # assert all Model objects in db + assert Catalog.objects.count() == 2 + assert Section.objects.count() == 6 + assert PageModel.objects.count() == 48 + assert QuestionSet.objects.count() == 3 + assert Question.objects.count() == 89 diff --git a/rdmo/management/tests/test_frontend.py b/rdmo/management/tests/e2e/test_frontend_management_elements.py similarity index 52% rename from rdmo/management/tests/test_frontend.py rename to rdmo/management/tests/e2e/test_frontend_management_elements.py index 3bf05f75d1..47c51e9483 100644 --- a/rdmo/management/tests/test_frontend.py +++ b/rdmo/management/tests/e2e/test_frontend_management_elements.py @@ -1,101 +1,22 @@ -import os +# ruff: noqa: F811 import re -from dataclasses import dataclass from urllib.parse import urlparse import pytest -from django.core.management import call_command - from playwright.sync_api import Page, expect -from pytest_django.live_server_helper import LiveServer -from rdmo.accounts.utils import set_group_permissions from rdmo.conditions.models import Condition -from rdmo.core.models import Model from rdmo.domain.models import Attribute -from rdmo.options.models import Option, OptionSet -from rdmo.questions.models import Catalog, Question, Section -from rdmo.questions.models import Page as PageModel -from rdmo.questions.models.questionset import QuestionSet -from rdmo.tasks.models import Task -from rdmo.views.models import View +from rdmo.management.tests.helpers_models import ModelHelper, model_helpers +from rdmo.questions.models import Catalog pytestmark = pytest.mark.e2e -# needed for playwright to run -os.environ.setdefault("DJANGO_ALLOW_ASYNC_UNSAFE", "true") - - -@dataclass -class ModelHelper: - """Helper class to bundle information about models for test cases.""" - - model: Model - form_field: str = "URI Path" - db_field: str = "uri_path" - has_nested: bool = False - - @property - def url(self) -> str: - return f"{self.model._meta.model_name}s" - - @property - def verbose_name(self) -> str: - """Return the verbose_name for the model.""" - return self.model._meta.verbose_name - - @property - def verbose_name_plural(self) -> str: - """Return the verbose_name_plural for the model.""" - return self.model._meta.verbose_name_plural - - -@pytest.fixture(scope="function") -def e2e_tests_django_db_setup(django_db_setup, django_db_blocker, fixtures): - """Set up database and populate with fixtures, that get restored for every test case.""" - with django_db_blocker.unblock(): - call_command("loaddata", *fixtures) - set_group_permissions() - - -@pytest.fixture(scope="session") -def base_url(live_server: LiveServer) -> str: - """Enable playwright to address URLs with base URL automatically prefixed.""" - return live_server.url - - -@pytest.fixture -def logged_in_admin_user(e2e_tests_django_db_setup, page: Page) -> Page: - """Log in as admin user through django login UI, returns logged in page for e2e tests.""" - page.goto("/account/login") - page.get_by_label("Username").fill("admin", timeout=5000) - page.get_by_label("Password").fill("admin") - page.get_by_role("button", name="Login").click() - page.goto("/management") - yield page - - -model_helpers = ( - ModelHelper(Catalog, has_nested=True), - ModelHelper(Section, has_nested=True), - ModelHelper(PageModel, has_nested=True), - ModelHelper(QuestionSet, has_nested=True), - ModelHelper(Question), - ModelHelper( - Attribute, has_nested=True, form_field="Key", db_field="key" - ), - ModelHelper(OptionSet, has_nested=True), - ModelHelper(Option), - ModelHelper(Condition), - ModelHelper(Task), - ModelHelper(View), -) @pytest.mark.parametrize("helper", model_helpers) -def test_management_navigation(logged_in_admin_user: Page, helper: ModelHelper) -> None: +def test_management_navigation(page: Page, helper: ModelHelper) -> None: """Test that each content type is available through the navigation.""" - page = logged_in_admin_user expect(page.get_by_role("heading", name="Management")).to_be_visible() # click a link in the navigation @@ -115,9 +36,8 @@ def test_management_navigation(logged_in_admin_user: Page, helper: ModelHelper) @pytest.mark.parametrize("helper", model_helpers) -def test_management_has_items(logged_in_admin_user: Page, helper: ModelHelper) -> None: +def test_management_has_items(page: Page, helper: ModelHelper) -> None: """Test all items in database are visible in management UI.""" - page = logged_in_admin_user num_items_in_database = helper.model.objects.count() page.goto(f"/management/{helper.url}") items_in_ui = page.locator(".list-group > .list-group-item") @@ -125,11 +45,8 @@ def test_management_has_items(logged_in_admin_user: Page, helper: ModelHelper) - @pytest.mark.parametrize("helper", model_helpers) -def test_management_nested_view( - logged_in_admin_user: Page, helper: ModelHelper -) -> None: +def test_management_nested_view(page: Page, helper: ModelHelper) -> None: """For each element type, that has a nested view, click the first example.""" - page = logged_in_admin_user page.goto(f"/management/{helper.url}") # Open nested view for element type if helper.has_nested: @@ -139,11 +56,8 @@ def test_management_nested_view( @pytest.mark.parametrize("helper", model_helpers) -def test_management_create_model( - logged_in_admin_user: Page, helper: ModelHelper -) -> None: +def test_management_create_model(page: Page, helper: ModelHelper) -> None: """Test management UI can create objects in the database.""" - page = logged_in_admin_user num_objects_at_start = helper.model.objects.count() page.goto(f"/management/{helper.url}") # click "New" button @@ -153,11 +67,7 @@ def test_management_create_model( page.get_by_label(helper.form_field).fill(value) if helper.model == Condition: # conditions need to have a source attribute - source_form = ( - page.locator(".form-group") - .filter(has_text="Source") - .locator(".select-item > .react-select") - ) + source_form = page.locator(".form-group").filter(has_text="Source").locator(".select-item > .react-select") source_form.click() page.keyboard.type(Attribute.objects.first().uri) page.keyboard.press("Enter") @@ -175,8 +85,7 @@ def test_management_create_model( @pytest.mark.parametrize("helper", model_helpers) -def test_management_edit_model(logged_in_admin_user: Page, helper: ModelHelper) -> None: - page = logged_in_admin_user +def test_management_edit_model(page: Page, helper: ModelHelper) -> None: page.goto(f"/management/{helper.url}") # click edit edit_button_title = f"Edit {helper.verbose_name}" diff --git a/rdmo/management/tests/helpers_import_elements.py b/rdmo/management/tests/helpers_import_elements.py new file mode 100644 index 0000000000..d50b923952 --- /dev/null +++ b/rdmo/management/tests/helpers_import_elements.py @@ -0,0 +1,85 @@ +import random +from collections import OrderedDict +from functools import partial +from typing import Dict, List, Optional, Tuple, Union + +from rdmo.core.imports import ImportElementFields, track_changes_on_element +from rdmo.management.import_utils import initialize_import_element_dict +from rdmo.management.imports import import_elements +from rdmo.management.tests.helpers_xml import read_xml_and_parse_to_root_and_elements + +IMPORT_ELEMENT_PANELS_LOCATOR = ".list-group > .list-group-item > .checkbox" +IMPORT_ELEMENT_PANELS_LOCATOR_SHOWN = ".list-group > .list-group-item > .row" + +UPDATE_FIELD_FUNCS = { + 'comment': lambda text: f"this is a test comment {text}", + 'target_text': lambda text: f"test target_text {text}", + 'relation': lambda text: "notempty".format(), +} + + +def filter_changed_fields(element, updated_fields=None) -> bool: + _changed = element.get('changed', False) + if updated_fields is None: + return _changed + changes = element.get(ImportElementFields.DIFF, {}) + for field, diff in changes.items(): + if field not in updated_fields: + continue + _new_value = diff.get(ImportElementFields.NEW) + _current_value = diff.get(ImportElementFields.CURRENT) + if _new_value != _current_value: + return True + return _changed + +def get_changed_elements(elements: List[Dict]) -> Dict[str, Dict[str,Union[bool,str]]]: + changed_elements = {} + for element in elements: + + changed_fields = [] + for field, diff_field in element[ImportElementFields.DIFF].items(): + if not (diff_field[ImportElementFields.NEW] == diff_field[ImportElementFields.CURRENT]): + changed_fields.append(field) + if changed_fields: + changed_elements[element['uri']] = {} + changed_elements[element['uri']]['changed'] = bool(changed_fields) + changed_elements[element['uri']]['changed_fields'] = changed_fields + return changed_elements + + +def _test_helper_filter_updated_and_changed(elements: List[Dict], updated_fields: Optional[Tuple]) -> List[Dict]: + filter_func = partial(filter_changed_fields, updated_fields=updated_fields) + changed_elements = filter(filter_func, elements) + return list(changed_elements) + + +def _test_helper_change_fields_elements(elements, + fields_to_update: Optional[Tuple] = None, + n=3) -> OrderedDict: + """ elements test preparation function """ + + if len(elements) < n: + raise ValueError("Length of elements should not be smaller than n.") + _new_elements = OrderedDict() + for _n, (_uri, _element) in enumerate(elements.items()): + if _n <= n - 1: + initialize_import_element_dict(_element) + for field in fields_to_update: + original_value = _element[field] or '' + new_val = UPDATE_FIELD_FUNCS[field](_n) + track_changes_on_element(_element, field, new_val, original_value=original_value) + _element[field] = new_val + _new_elements[_uri] = _element + return _new_elements + + +def parse_xml_and_import_elements(xml_file, shuffle_elements=False): + elements, root = read_xml_and_parse_to_root_and_elements(xml_file) + if shuffle_elements: + # Extract items from the OrderedDict + items = list(elements.items()) + # Shuffle the list of items + random.shuffle(items) + elements = OrderedDict(items) + imported_elements = import_elements(elements) + return elements, root, imported_elements diff --git a/rdmo/management/tests/helpers_models.py b/rdmo/management/tests/helpers_models.py new file mode 100644 index 0000000000..bc0042662f --- /dev/null +++ b/rdmo/management/tests/helpers_models.py @@ -0,0 +1,59 @@ +from dataclasses import dataclass +from typing import List + +from rdmo.conditions.models import Condition +from rdmo.core.models import Model +from rdmo.domain.models import Attribute +from rdmo.options.models import Option, OptionSet +from rdmo.questions.models import Catalog, Question, Section +from rdmo.questions.models import Page as PageModel +from rdmo.questions.models.questionset import QuestionSet +from rdmo.tasks.models import Task +from rdmo.views.models import View + + +@dataclass +class ModelHelper: + """Helper class to bundle information about models for test cases.""" + + model: Model + form_field: str = "URI Path" + db_field: str = "uri_path" + has_nested: bool = False + + @property + def url(self) -> str: + return f"{self.model._meta.model_name}s" + + @property + def verbose_name(self) -> str: + """Return the verbose_name for the model.""" + return self.model._meta.verbose_name + + @property + def verbose_name_plural(self) -> str: + """Return the verbose_name_plural for the model.""" + return self.model._meta.verbose_name_plural + + + +model_helpers = ( + ModelHelper(Catalog, has_nested=True), + ModelHelper(Section, has_nested=True), + ModelHelper(PageModel, has_nested=True), + ModelHelper(QuestionSet, has_nested=True), + ModelHelper(Question), + ModelHelper( + Attribute, has_nested=True, form_field="Key", db_field="key" + ), + ModelHelper(OptionSet, has_nested=True), + ModelHelper(Option), + ModelHelper(Condition), + ModelHelper(Task), + ModelHelper(View), +) + + +def delete_all_objects(db_models: List): + for db_model in db_models: + db_model.objects.all().delete() diff --git a/rdmo/management/tests/helpers_xml.py b/rdmo/management/tests/helpers_xml.py new file mode 100644 index 0000000000..d5fd64a420 --- /dev/null +++ b/rdmo/management/tests/helpers_xml.py @@ -0,0 +1,42 @@ + +from rdmo.core.xml import parse_xml_to_elements, read_xml, resolve_file + +xml_test_files = { + "xml/elements/catalogs.xml": + None, + "xml/elements/updated-and-changed/optionsets-1.xml": + None, + 'file-does-not-exist.xml': + 'This file does not exists', + "xml/error.xml": + "The content of the XML file does not consist of well-formed data or markup. XML Parsing Error: syntax error: line 1, column 0", # noqa: E501 + "xml/project.xml": + "This XML does not contain RDMO content.", + 'xml/error-version.xml': + 'This RDMO XML file does not have a valid version number. XML Version (99.9.9) is greater', + 'xml/elements/legacy/catalog-error-key.xml': + 'XML Parsing Error: Missing legacy elements', +} + +xml_error_files = {k: v for k,v in xml_test_files.items() if v is not None} +xml_error_files['file-does-not-exist.xml'] = 'This field may not be blank.' + +def read_xml_and_parse_to_root_and_elements(file): + errors = [] + + xml_file, file_error = resolve_file(file) + if file_error: + errors.append(file_error) + + root, read_error = read_xml(xml_file) + if read_error: + errors.append(read_error) + + xml_parsed_elements, xml_parsing_errors = parse_xml_to_elements(xml_file=xml_file) + if xml_parsing_errors: + errors.extend(xml_parsing_errors) + + if errors: + _msg = "\n".join(map(str, xml_parsing_errors)) + raise ValueError(f"This test function should NOT raise any Exceptions. {_msg!s}") + return xml_parsed_elements, root diff --git a/rdmo/management/tests/test_commands.py b/rdmo/management/tests/test_commands.py index b9b252d776..e3263b1925 100644 --- a/rdmo/management/tests/test_commands.py +++ b/rdmo/management/tests/test_commands.py @@ -6,32 +6,21 @@ from django.core.management import call_command from django.core.management.base import CommandError +from rdmo.management.tests.helpers_xml import xml_test_files -def test_import(db, settings): - xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'catalogs.xml' - stdout, stderr = io.StringIO(), io.StringIO() - - call_command('import', xml_file, stdout=stdout, stderr=stderr) - - assert not stdout.getvalue() - assert not stderr.getvalue() - -def test_import_error(db, settings): - xml_file = Path(settings.BASE_DIR) / 'xml' / 'error.xml' +@pytest.mark.parametrize("xml_file_path,error_message", xml_test_files.items()) +def test_import(db, settings, xml_file_path, error_message): + xml_file = Path(settings.BASE_DIR).joinpath(xml_file_path) stdout, stderr = io.StringIO(), io.StringIO() - with pytest.raises(CommandError) as e: + if error_message is None: call_command('import', xml_file, stdout=stdout, stderr=stderr) - assert str(e.value) == 'The content of the xml file does not consist of well formed data or markup.' - - -def test_import_error2(db, settings): - xml_file = Path(settings.BASE_DIR) / 'xml' / 'project.xml' - stdout, stderr = io.StringIO(), io.StringIO() - - with pytest.raises(CommandError) as e: - call_command('import', xml_file, stdout=stdout, stderr=stderr) + assert not stdout.getvalue() + assert not stderr.getvalue() + else: + with pytest.raises(CommandError) as e: + call_command('import', xml_file, stdout=stdout, stderr=stderr) - assert str(e.value) == 'This XML does not contain RDMO content.' + assert str(e.value).startswith(error_message) diff --git a/rdmo/management/tests/test_import_conditions.py b/rdmo/management/tests/test_import_conditions.py index 4c97ced3fd..01a52d99b5 100644 --- a/rdmo/management/tests/test_import_conditions.py +++ b/rdmo/management/tests/test_import_conditions.py @@ -1,73 +1,76 @@ from pathlib import Path +import pytest + from rdmo.conditions.models import Condition -from rdmo.core.xml import convert_elements, flat_xml_to_elements, order_elements, read_xml_file +from rdmo.core.imports import ImportElementFields from rdmo.management.imports import import_elements +from .helpers_import_elements import ( + _test_helper_change_fields_elements, + _test_helper_filter_updated_and_changed, + parse_xml_and_import_elements, +) +from .helpers_xml import read_xml_and_parse_to_root_and_elements + +fields_to_be_changed = (('comment',),) def test_create_conditions(db, settings): Condition.objects.all().delete() xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'conditions.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == Condition.objects.count() == 15 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert len(root) == len(imported_elements) == Condition.objects.count() == 15 + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_conditions(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'conditions.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 15 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + - assert len(root) == len(elements) - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_conditions_with_changed_fields(db, settings, updated_fields): + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'conditions.xml' + + elements, root = read_xml_and_parse_to_root_and_elements(xml_file) + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=7) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert len(root) == len(imported_elements) == 15 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] def test_create_legacy_conditions(db, settings): Condition.objects.all().delete() xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'conditions.xml' + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) - - assert len(root) == len(elements) == Condition.objects.count() == 15 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert len(root) == len(imported_elements) == Condition.objects.count() == 15 + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_legacy_conditions(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'conditions.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) - - assert len(root) == len(elements) == 15 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 15 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) diff --git a/rdmo/management/tests/test_import_domain.py b/rdmo/management/tests/test_import_domain.py index b9f032b92f..0386691313 100644 --- a/rdmo/management/tests/test_import_domain.py +++ b/rdmo/management/tests/test_import_domain.py @@ -1,42 +1,62 @@ from pathlib import Path -from rdmo.core.xml import convert_elements, flat_xml_to_elements, order_elements, read_xml_file +import pytest + +from rdmo.core.imports import ImportElementFields from rdmo.domain.models import Attribute from rdmo.management.imports import import_elements +from .helpers_import_elements import ( + _test_helper_change_fields_elements, + _test_helper_filter_updated_and_changed, + parse_xml_and_import_elements, +) +from .helpers_xml import read_xml_and_parse_to_root_and_elements + +fields_to_be_changed = (('comment',),) def test_create_domain(db, settings): Attribute.objects.all().delete() xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'attributes.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == Attribute.objects.count() == 86 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert len(root) == len(imported_elements) == Attribute.objects.count() == 86 + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_domain(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'attributes.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) - assert len(root) == len(elements) - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_attributes_with_changed_fields(db, settings, updated_fields): + _change_count = Attribute.objects.count() / 2 + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'attributes.xml' + elements, root = read_xml_and_parse_to_root_and_elements(xml_file) + # import initial elements from xml + _el = import_elements(elements, save=True) + # update the elements and call import again + updated_elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=_change_count) + changed_elements = _test_helper_filter_updated_and_changed(updated_elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(updated_elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + + assert len(root) == len(imported_elements) == 86 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] def test_create_legacy_domain(db, settings): @@ -44,31 +64,19 @@ def test_create_legacy_domain(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'domain.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == 86 + assert len(root) == len(imported_elements) == 86 assert Attribute.objects.count() == 86 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_legacy_domain(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'domain.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) - - assert len(root) == len(elements) == 86 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 86 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) diff --git a/rdmo/management/tests/test_import_options.py b/rdmo/management/tests/test_import_options.py index 65dd16a82b..1af5787096 100644 --- a/rdmo/management/tests/test_import_options.py +++ b/rdmo/management/tests/test_import_options.py @@ -1,109 +1,246 @@ from pathlib import Path -from rdmo.core.xml import convert_elements, flat_xml_to_elements, order_elements, read_xml_file +import pytest + +from rdmo.core.imports import ImportElementFields from rdmo.management.imports import import_elements from rdmo.options.models import Option, OptionSet +from .helpers_import_elements import ( + _test_helper_change_fields_elements, + _test_helper_filter_updated_and_changed, + get_changed_elements, + parse_xml_and_import_elements, +) +from .helpers_models import delete_all_objects +from .helpers_xml import read_xml_and_parse_to_root_and_elements + +fields_to_be_changed = (('comment',),) + +test_optionset = { + 'original': { + "uri": "http://example.com/terms/options/one_two_three", + "options": [ + 'http://example.com/terms/options/one_two_three/one', + 'http://example.com/terms/options/one_two_three/two', + 'http://example.com/terms/options/one_two_three/three', + ], + }, + } + +OPTIONSET_URIS = { + "http://example.com/terms/options/condition": [ + "http://example.com/terms/options/condition/other" + ], + "http://example.com/terms/options/one_two_three": [ + "http://example.com/terms/options/one_two_three/one", + "http://example.com/terms/options/one_two_three/two", + "http://example.com/terms/options/one_two_three/three", + ], + "http://example.com/terms/options/one_two_three_other": [ + "http://example.com/terms/options/one_two_three_other/one", + "http://example.com/terms/options/one_two_three_other/two", + "http://example.com/terms/options/one_two_three_other/three", + "http://example.com/terms/options/one_two_three_other/text", + "http://example.com/terms/options/one_two_three_other/textarea" + ], + "http://example.com/terms/options/plugin": [] +} +LEGACY_SKIP_URIS = [ + "http://example.com/terms/options/one_two_three_other/textarea" +] + def test_create_optionsets(db, settings): - OptionSet.objects.all().delete() - Option.objects.all().delete() + delete_all_objects([OptionSet, Option]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'optionsets.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) - - assert len(root) == len(elements) == 13 + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(elements) == len(imported_elements) == 13 assert OptionSet.objects.count() == 4 assert Option.objects.count() == 9 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) - + assert all(element[ImportElementFields.CREATED] is True for element in imported_elements) + assert all(element[ImportElementFields.UPDATED] is False for element in imported_elements) + for optionset_uri, options_uris in OPTIONSET_URIS.items(): + db_optionset = OptionSet.objects.get(uri=optionset_uri) + db_options = Option.objects.filter(uri__in=options_uris) + db_options_uris = db_options.values_list('uri', flat=True) + assert set(db_options_uris) == set(options_uris) + db_ordered_options_uris = db_optionset.options.filter(uri__in=options_uris).order_by( + 'option_optionsets__order').values_list('uri',flat=True) + assert options_uris == list(db_ordered_options_uris) def test_update_optionsets(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'optionsets.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + # Arrange, import the optionsets.xml + delete_all_objects([OptionSet, Option]) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + assert OptionSet.objects.count() == 4 + assert Option.objects.count() == 9 + + # Act, import the optionsets.xml again + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == 13 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + assert len(root) == len(elements) == len(imported_elements) == 13 + assert all(element[ImportElementFields.CREATED] is False for element in imported_elements) + assert all(element[ImportElementFields.UPDATED] is True for element in imported_elements) + assert OptionSet.objects.count() == 4 + assert Option.objects.count() == 9 + + +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_optionsets_with_changed_fields(db, settings, updated_fields): + delete_all_objects([OptionSet, Option]) + + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'optionsets.xml' + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + assert len(root) == len(imported_elements) == 13 + assert OptionSet.objects.count() + Option.objects.count() == 13 + # start test with fresh options in db + _n_change = int(Option.objects.count() / 2) + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=7) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + assert len(root) == len(imported_elements) == 13 + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert all(element[ImportElementFields.CREATED] is False for element in imported_elements) + assert all(element[ImportElementFields.UPDATED] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] + + +def test_update_optionsets_from_changed_xml(db, settings): + # Arrange, start test with fresh options in db + # Arrange, import the optionsets.xml + delete_all_objects([OptionSet, Option]) + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'optionsets.xml' + parse_xml_and_import_elements(xml_file) + assert OptionSet.objects.count() + Option.objects.count() == 13 + # Act, import from xml optionsets-1.xml that contains changes + xml_file_1 = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'updated-and-changed' / 'optionsets-1.xml' + elements_1, root_1 = read_xml_and_parse_to_root_and_elements(xml_file_1) + imported_elements_1 = import_elements(elements_1, save=False) + assert imported_elements_1 + assert [i for i in imported_elements_1 if i[ImportElementFields.DIFF]] + warnings_elements = [i for i in imported_elements_1 if i[ImportElementFields.WARNINGS]] + assert len(warnings_elements) == 2 + + changed_elements = get_changed_elements(imported_elements_1) + + assert test_optionset['original']['uri'] in changed_elements + assert len([i for i in changed_elements.values() if i]) == 5 + + # change the order of the options, as in the xml + optionset_element = next(filter(lambda x: x['uri'] == test_optionset['original']['uri'], imported_elements_1)) + # the test changes are simply the reversed order of the options + test_optionset_changed_options = test_optionset['original']['options'][::-1] + + assert optionset_element + assert "options" in optionset_element[ImportElementFields.DIFF] + assert optionset_element[ImportElementFields.DIFF]['options'][ImportElementFields.CURRENT] == test_optionset['original']['options'] # noqa: E501 + assert optionset_element[ImportElementFields.DIFF]['options'][ImportElementFields.NEW] == test_optionset_changed_options # noqa: E501 + + # now save the elements_1 + _imported_elements_1_save = import_elements(elements_1, save=True) + # get the ordered options (via .optionset_options) for this optionset from the db + optionset_1 = OptionSet.objects.get(uri=test_optionset['original']['uri']) + optionset_1_options = optionset_1.optionset_options.order_by('order').values_list('option__uri',flat=True) + for _test, _db in zip(test_optionset_changed_options, optionset_1_options): + assert _test == _db + + # Import again and test that there are no changes detected + imported_elements_2 = import_elements(elements_1, save=False) + changed_elements_2 = get_changed_elements(imported_elements_2) + assert len(changed_elements_2) == 0 + assert len([i for i in imported_elements_2 if i[ImportElementFields.WARNINGS]]) == 2 def test_create_options(db, settings): + # Arrange Option.objects.all().delete() - + # Act xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'options.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == Option.objects.count() == 9 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert len(root) == len(elements) == len(imported_elements) == Option.objects.count() == 9 + assert all(element[ImportElementFields.CREATED] is True for element in imported_elements) + assert all(element[ImportElementFields.UPDATED] is False for element in imported_elements) def test_update_options(db, settings): + # Arrange + Option.objects.all().delete() xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'options.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + parse_xml_and_import_elements(xml_file) + assert Option.objects.count() == 9 + + # Act + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(elements) == len(imported_elements) == Option.objects.count() == 9 + assert all(element[ImportElementFields.CREATED] is False for element in imported_elements) + assert all(element[ImportElementFields.UPDATED] is True for element in imported_elements) + + +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_options_with_changed_fields(db, settings, updated_fields): + delete_all_objects([OptionSet, Option]) - assert len(root) == len(elements) == 9 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'options.xml' + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + assert len(root) == len(imported_elements) == 9 + # start test with fresh options in db + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=4) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert len(root) == len(imported_elements) == 9 + assert all(element[ImportElementFields.CREATED] is False for element in imported_elements) + assert all(element[ImportElementFields.UPDATED] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] def test_create_legacy_options(db, settings): - OptionSet.objects.all().delete() - Option.objects.all().delete() + delete_all_objects([OptionSet, Option]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'options.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == 12 + assert len(root) == len(elements) == len(imported_elements) == 12 assert OptionSet.objects.count() == 4 assert Option.objects.count() == 8 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert all(element[ImportElementFields.CREATED] is True for element in imported_elements) + assert all(element[ImportElementFields.UPDATED] is False for element in imported_elements) + for optionset_uri, test_options_uris in OPTIONSET_URIS.items(): + # legacy has no "http://example.com/terms/options/one_two_three_other/textarea" + options_uris = [i for i in test_options_uris if i not in LEGACY_SKIP_URIS] + db_optionset = OptionSet.objects.get(uri=optionset_uri) + db_options = Option.objects.filter(uri__in=options_uris) + db_options_uris = db_options.values_list('uri', flat=True) + assert set(db_options_uris) == set(options_uris) + db_ordered_options_uris = db_optionset.options.filter(uri__in=options_uris).order_by( + 'option_optionsets__order').values_list('uri',flat=True) + assert options_uris == list(db_ordered_options_uris) def test_update_legacy_options(db, settings): + delete_all_objects([OptionSet, Option]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'options.xml' + parse_xml_and_import_elements(xml_file) + assert OptionSet.objects.count() == 4 + assert Option.objects.count() == 8 - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) - - assert len(root) == len(elements) == 12 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(elements) == len(imported_elements) == 12 + assert OptionSet.objects.count() == 4 + assert Option.objects.count() == 8 + assert all(element[ImportElementFields.CREATED] is False for element in imported_elements) + assert all(element[ImportElementFields.UPDATED] is True for element in imported_elements) diff --git a/rdmo/management/tests/test_import_questions.py b/rdmo/management/tests/test_import_questions.py index 7bb275fce2..fadf5127d6 100644 --- a/rdmo/management/tests/test_import_questions.py +++ b/rdmo/management/tests/test_import_questions.py @@ -1,259 +1,319 @@ from pathlib import Path -from rdmo.core.xml import convert_elements, flat_xml_to_elements, order_elements, read_xml_file +import pytest + +from rdmo.core.imports import ImportElementFields from rdmo.management.imports import import_elements from rdmo.questions.models import Catalog, Page, Question, QuestionSet, Section +from .helpers_import_elements import ( + _test_helper_change_fields_elements, + _test_helper_filter_updated_and_changed, + parse_xml_and_import_elements, +) +from .helpers_models import delete_all_objects + +fields_to_be_changed = (('comment',),) + +TEST_CATALOG_SECTIONS_URIS = { + "http://example.com/terms/questions/catalog/individual", + "http://example.com/terms/questions/catalog/collections", + "http://example.com/terms/questions/catalog/set", + "http://example.com/terms/questions/catalog/conditions", + "http://example.com/terms/questions/catalog/options", + "http://example.com/terms/questions/catalog/blocks" +} -def test_create_catalogs(db, settings): - Catalog.objects.all().delete() - Section.objects.all().delete() - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() + +@pytest.mark.parametrize('shuffle', [True, False]) +def test_create_catalogs(db, settings, shuffle): + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'catalogs.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file, shuffle_elements=shuffle) - assert len(root) == len(elements) == 148 + assert len(root) == len(imported_elements) == 148 assert Catalog.objects.count() == 2 assert Section.objects.count() == 6 assert Page.objects.count() == 48 assert QuestionSet.objects.count() == 3 assert Question.objects.count() == 89 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) + # check that all elements ended up in the catalog + catalog = Catalog.objects.prefetch_elements().get(uri="http://example.com/terms/questions/catalog") + catalog_sections = catalog.sections.all() + catalog_sections_uris = set(catalog_sections.values_list('uri', flat=True)) + assert catalog_sections_uris == TEST_CATALOG_SECTIONS_URIS + + sections_pages = Section.objects.filter(uri__in=catalog_sections_uris).values_list('pages') + assert sections_pages.distinct().count() == 48 + sections_pages_questionsets = Page.objects.filter(id__in=sections_pages).values_list('questionsets') + assert sections_pages_questionsets.distinct().count() == 3 + sections_pages_questions = Page.objects.filter(id__in=sections_pages).values_list('questions') + assert sections_pages_questions.distinct().count() == 85 def test_update_catalogs(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'catalogs.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 148 - assert len(root) == len(elements) == 148 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + + +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_catalogs_with_changed_fields(db, settings, updated_fields): + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) + + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'catalogs.xml' + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + assert len(root) == len(imported_elements) == 148 + # start test with fresh elements in db + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=75) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert len(imported_elements) == 148 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] def test_create_sections(db, settings): - Section.objects.all().delete() - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() + delete_all_objects([Section, Page, QuestionSet, Question]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'sections.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == 146 + assert len(root) == len(imported_elements) == 146 assert Section.objects.count() == 6 assert Page.objects.count() == 48 assert QuestionSet.objects.count() == 3 assert Question.objects.count() == 89 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_sections(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'sections.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 146 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) - assert len(root) == len(elements) == 146 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_sections_with_changed_fields(db, settings, updated_fields): + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) + + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'sections.xml' + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + assert len(root) == len(imported_elements) == 146 + # start test with fresh elements in db + + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=75) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] def test_create_pages(db, settings): - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() + delete_all_objects([Page, QuestionSet, Question]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'pages.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == 140 + assert len(root) == len(imported_elements) == 140 assert Page.objects.count() == 48 assert QuestionSet.objects.count() == 3 assert Question.objects.count() == 89 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_pages(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'pages.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 140 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) - assert len(root) == len(elements) == 140 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_pages_with_changed_fields(db, settings, updated_fields): + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) + + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'pages.xml' + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + assert len(root) == len(imported_elements) == 140 + # start test with fresh elements in db + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=75) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert len(imported_elements) == 140 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] def test_create_questionsets(db, settings): - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() + delete_all_objects([Page, QuestionSet, Question]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'questionsets.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) assert len(root) == 10 # two questionsets appear twice in the export file - assert len(elements) == 8 + assert len(imported_elements) == 8 assert QuestionSet.objects.count() == 3 assert Question.objects.count() == 5 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_questionsets(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'questionsets.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) assert len(root) == 10 # two questionsets appear twice in the export file - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + assert len(imported_elements) == 8 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) -def test_create_questions(db, settings): - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_questionsets_with_changed_fields(db, settings, updated_fields): + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) + + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'questionsets.xml' + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + assert len(root) == 10 # two questionsets appear twice in the export file + assert len(imported_elements) == 8 + # start test with fresh elements in db + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=5) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert len(imported_elements) == 8 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] + + +@pytest.mark.parametrize('shuffle', [True, False]) +def test_create_questions(db, settings, shuffle): + delete_all_objects([Page, QuestionSet, Question]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'questions.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file, shuffle_elements=shuffle) - assert len(root) == len(elements) == 89 + assert len(root) == len(imported_elements) == 89 assert Question.objects.count() == 89 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_questions(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'questions.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == 89 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + assert len(root) == len(imported_elements) == 89 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) -def test_create_legacy_questions(db, settings): - Catalog.objects.all().delete() - Section.objects.all().delete() - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_questions_with_changed_fields(db, settings, updated_fields): + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) + + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'questions.xml' + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + assert len(root) == len(imported_elements) == 89 + # start test with fresh elements in db + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=45) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert len(imported_elements) == 89 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] + + +@pytest.mark.parametrize('shuffle', [True, False]) +def test_create_legacy_questions(db, settings, shuffle): + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'questions.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file, shuffle_elements=shuffle) - assert len(root) == len(elements) == 147 + assert len(root) == len(imported_elements) == 147 assert Catalog.objects.count() == 1 assert Section.objects.count() == 6 assert Page.objects.count() == 48 assert QuestionSet.objects.count() == 3 assert Question.objects.count() == 89 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) # check that all elements ended up in the catalog catalog = Catalog.objects.prefetch_elements().first() - descendant_uris = {element.uri for element in catalog.descendants} - element_uris = {element['uri'] for element in elements if element['uri'] != catalog.uri} - assert descendant_uris == element_uris + catalog_sections = catalog.sections.all() + catalog_sections_uris = set(catalog_sections.values_list('uri', flat=True)) + assert catalog_sections_uris == TEST_CATALOG_SECTIONS_URIS + sections_pages = Section.objects.filter(uri__in=catalog_sections_uris).values_list('pages') + assert sections_pages.distinct().count() == 48 + sections_pages_questionsets = Page.objects.filter(id__in=sections_pages).values_list('questionsets') + assert sections_pages_questionsets.distinct().count() == 3 + sections_pages_questions = Page.objects.filter(id__in=sections_pages).values_list('questions') + assert sections_pages_questions.distinct().count() == 85 def test_update_legacy_questions(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'questions.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == 147 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + assert len(root) == len(imported_elements) == 147 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) # check that all elements ended up in the catalog catalog = Catalog.objects.prefetch_elements().first() - descendant_uris = {element.uri for element in catalog.descendants} - element_uris = {element['uri'] for element in elements if element['uri'] != catalog.uri} - assert descendant_uris == element_uris + catalog_sections = catalog.sections.all() + catalog_sections_uris = set(catalog_sections.values_list('uri', flat=True)) + assert catalog_sections_uris == TEST_CATALOG_SECTIONS_URIS diff --git a/rdmo/management/tests/test_import_tasks.py b/rdmo/management/tests/test_import_tasks.py index 08071990cf..bd88542bcb 100644 --- a/rdmo/management/tests/test_import_tasks.py +++ b/rdmo/management/tests/test_import_tasks.py @@ -1,42 +1,59 @@ from pathlib import Path -from rdmo.core.xml import convert_elements, flat_xml_to_elements, order_elements, read_xml_file +import pytest + +from rdmo.core.imports import ImportElementFields from rdmo.management.imports import import_elements from rdmo.tasks.models import Task +from .helpers_import_elements import ( + _test_helper_change_fields_elements, + _test_helper_filter_updated_and_changed, + parse_xml_and_import_elements, +) +from .helpers_xml import read_xml_and_parse_to_root_and_elements + +fields_to_be_changed = (('comment',),) + def test_create_tasks(db, settings): Task.objects.all().delete() xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'tasks.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == Task.objects.count() == 2 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert len(root) == len(imported_elements) == Task.objects.count() == 2 + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_tasks(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'tasks.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 2 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) - assert len(root) == len(elements) == 2 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_tasks_with_changed_fields(db, settings, updated_fields): + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'tasks.xml' + + elements, root = read_xml_and_parse_to_root_and_elements(xml_file) + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=1) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert len(root) == len(imported_elements) == 2 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] def test_create_legacy_tasks(db, settings): @@ -44,30 +61,18 @@ def test_create_legacy_tasks(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'tasks.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == Task.objects.count() == 2 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert len(root) == len(imported_elements) == Task.objects.count() == 2 + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_legacy_tasks(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'tasks.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) - - assert len(root) == len(elements) == 2 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 2 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) diff --git a/rdmo/management/tests/test_import_views.py b/rdmo/management/tests/test_import_views.py index a6c7bc7d06..7fe08f1378 100644 --- a/rdmo/management/tests/test_import_views.py +++ b/rdmo/management/tests/test_import_views.py @@ -1,42 +1,59 @@ from pathlib import Path -from rdmo.core.xml import convert_elements, flat_xml_to_elements, order_elements, read_xml_file +import pytest + +from rdmo.core.imports import ImportElementFields from rdmo.management.imports import import_elements from rdmo.views.models import View +from .helpers_import_elements import ( + _test_helper_change_fields_elements, + _test_helper_filter_updated_and_changed, + parse_xml_and_import_elements, +) +from .helpers_xml import read_xml_and_parse_to_root_and_elements + +fields_to_be_changed = (('comment',),) + def test_create_tasks(db, settings): View.objects.all().delete() xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'views.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == View.objects.count() == 3 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert len(root) == len(imported_elements) == View.objects.count() == 3 + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_tasks(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'views.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 3 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) - assert len(root) == len(elements) == 3 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + +@pytest.mark.parametrize('updated_fields', fields_to_be_changed) +def test_update_views_with_changed_fields(db, settings, updated_fields): + xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'views.xml' + + elements, root = read_xml_and_parse_to_root_and_elements(xml_file) + elements = _test_helper_change_fields_elements(elements, fields_to_update=updated_fields, n=2) + changed_elements = _test_helper_filter_updated_and_changed(elements.values(), updated_fields=updated_fields) + imported_elements = import_elements(elements) + imported_and_changed = _test_helper_filter_updated_and_changed(imported_elements, updated_fields=updated_fields) + assert len(root) == len(imported_elements) == 3 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) + assert len(imported_and_changed) == len(changed_elements) + # compare two ordered lists with "updated_and_changed" dicts + for test, imported in zip(changed_elements, imported_and_changed): + assert test[ImportElementFields.DIFF] == imported[ImportElementFields.DIFF] def test_create_legacy_tasks(db, settings): @@ -44,30 +61,18 @@ def test_create_legacy_tasks(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'views.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) - assert len(root) == len(elements) == View.objects.count() == 3 - assert all(element['created'] is True for element in elements) - assert all(element['updated'] is False for element in elements) + assert len(root) == len(imported_elements) == View.objects.count() == 3 + assert all(element['created'] is True for element in imported_elements) + assert all(element['updated'] is False for element in imported_elements) def test_update_legacy_tasks(db, settings): xml_file = Path(settings.BASE_DIR) / 'xml' / 'elements' / 'legacy' / 'views.xml' - root = read_xml_file(xml_file) - version = root.attrib.get('version') - elements = flat_xml_to_elements(root) - elements = convert_elements(elements, version) - elements = order_elements(elements) - elements = elements.values() - import_elements(elements) - - assert len(root) == len(elements) == 3 - assert all(element['created'] is False for element in elements) - assert all(element['updated'] is True for element in elements) + elements, root, imported_elements = parse_xml_and_import_elements(xml_file) + + assert len(root) == len(imported_elements) == 3 + assert all(element['created'] is False for element in imported_elements) + assert all(element['updated'] is True for element in imported_elements) diff --git a/rdmo/management/tests/test_merge_attributes.py b/rdmo/management/tests/test_merge_attributes.py new file mode 100644 index 0000000000..5d18ee9dea --- /dev/null +++ b/rdmo/management/tests/test_merge_attributes.py @@ -0,0 +1,320 @@ +import io +from string import Template +from typing import List, Union + +import pytest + +from django.core.management import CommandError, call_command + +from rdmo.conditions.models import Condition +from rdmo.domain.models import Attribute +from rdmo.options.models import Option +from rdmo.questions.models import Page, Question, QuestionSet, Section +from rdmo.views.models import View + +ElementType = Union[Section, Page, QuestionSet, Question, Option, Condition] + +ATTRIBUTE_RELATED_MODELS_FIELDS = [i for i in Attribute._meta.get_fields() + if i.is_relation and not i.many_to_many + and i.related_model is not Attribute] + +EXAMPLE_URI_PREFIX = 'http://example.com/terms' +FOO_MERGE_URI_PREFIX = 'http://foo-merge.com/terms' +BAR_MERGE_URI_PREFIX = 'http://bar-merge.com/terms' +EXAMPLE_VIEW_URI_PATH = "views/view_a" +VIEW_TEMPLATE_URI_PATH = 'individual/single/textarea' +VIEW_TEMPLATE_URI_PATH_ADDITIONS = ['', '/test1', '/test2', '/testfoo'] +VIEW_TEMPLATE_RENDER_VALUE = Template("{% render_value '$new_uri' %}") +EXAMPLE_VIEW_URI = Attribute.build_uri(EXAMPLE_URI_PREFIX, VIEW_TEMPLATE_URI_PATH) +NEW_MERGE_URI_PREFIXES = [FOO_MERGE_URI_PREFIX, BAR_MERGE_URI_PREFIX] + + +def _prepare_instance_for_copy(instance, uri_prefix=None, uri_path=None): + instance.pk = None + instance.id = None + instance._state.adding = True + if uri_prefix: + instance.uri_prefix = uri_prefix + if uri_path: + instance.uri_path = uri_path + return instance + + +def _get_queryset(related_field, attribute=None): + model = related_field.related_model + if model is View: + return model.objects.filter(**{"template__contains": attribute.path}) + lookup_field = related_field.remote_field.name + return model.objects.filter(**{lookup_field: attribute}) + + +def create_new_uris_with_uri_prefix_for_template(new_uri_prefix: str) -> List[str]: + new_uris = [] + for extra_path in VIEW_TEMPLATE_URI_PATH_ADDITIONS: + new_uri_path = VIEW_TEMPLATE_URI_PATH + extra_path + new_uri = Attribute.build_uri(new_uri_prefix, new_uri_path) + new_uris.append(new_uri) + return new_uris + + +def create_copy_of_view_that_uses_new_attribute(db, new_prefixes: List[str]): + qs = View.objects.filter(**{"uri__contains": EXAMPLE_VIEW_URI_PATH}).all() + if not qs.exists(): + raise ValueError("Views for tests should exist here.") + for instance in qs: + original_template = instance.template + for new_prefix in new_prefixes: + instance = _prepare_instance_for_copy(instance, uri_prefix=new_prefix, uri_path=EXAMPLE_VIEW_URI_PATH) + new_template_uris = create_new_uris_with_uri_prefix_for_template(new_prefix) + new_template = '' + new_template += original_template + for uri in new_template_uris: + new_template += '\n' + new_template += VIEW_TEMPLATE_RENDER_VALUE.substitute(new_uri=uri) + instance.template = new_template + instance.save() + + +def create_copies_of_related_models_with_new_uri_prefix(new_prefixes): + for related_model_field in ATTRIBUTE_RELATED_MODELS_FIELDS: + model = related_model_field.related_model + lookup_field = related_model_field.remote_field.name + # create new model instances from example.com objects with the new uri_prefix + filter_kwargs = {f"{lookup_field}__uri_prefix": EXAMPLE_URI_PREFIX} + example_objects = model.objects.filter(**filter_kwargs) + + for new_prefix in new_prefixes: + + if not example_objects: + continue + for instance in example_objects: + instance = _prepare_instance_for_copy(instance, uri_prefix=new_prefix) + current_attribute = getattr(instance, lookup_field) + if not isinstance(current_attribute, Attribute): + continue + filter_kwargs = {'path': current_attribute.path, 'uri_prefix': new_prefix} + new_attribute = Attribute.objects.filter(**filter_kwargs).first() + setattr(instance, lookup_field, new_attribute) + instance.save() + + +def create_copies_of_attributes_with_new_uri_prefix(example_attributes, new_prefixes): + for attribute in example_attributes: + for new_prefix in new_prefixes: + attribute = _prepare_instance_for_copy(attribute, uri_prefix=new_prefix) + attribute.save() + + +def get_related_affected_instances(attribute) -> list: + related_qs = [] + for related_field in ATTRIBUTE_RELATED_MODELS_FIELDS: + model = related_field.related_model + lookup_field = related_field.remote_field.name + qs = model.objects.filter(**{lookup_field: attribute}) + related_qs.append(qs) + return related_qs + + +@pytest.fixture +def _create_new_merge_attributes_and_views(db, settings): + """ Creates model instances for merge attributes tests """ + example_attributes = Attribute.objects.filter(uri_prefix=EXAMPLE_URI_PREFIX).all() + create_copies_of_attributes_with_new_uri_prefix(example_attributes, NEW_MERGE_URI_PREFIXES) + create_copies_of_related_models_with_new_uri_prefix(NEW_MERGE_URI_PREFIXES) + create_copy_of_view_that_uses_new_attribute(db, NEW_MERGE_URI_PREFIXES) + + +@pytest.mark.usefixtures("_create_new_merge_attributes_and_views") +def test_command_merge_attributes_fails_correctly(db, settings): + first_parent_attribute = Attribute.objects.exclude(parent=None).first().parent + first_leaf_attribute = None + for attribute in Attribute.objects.all(): + if not attribute.get_descendants().exists(): + first_leaf_attribute = attribute + break + + source_and_target_are_the_same = { + 'source': first_parent_attribute, + 'target': first_parent_attribute + } + stdout, stderr = io.StringIO(), io.StringIO() + with pytest.raises(CommandError): + call_command('merge_attributes', + stdout=stdout, stderr=stderr, **source_and_target_are_the_same) + + source_has_descendants = { + 'source': first_parent_attribute, + 'target': first_leaf_attribute + } + stdout, stderr = io.StringIO(), io.StringIO() + with pytest.raises(CommandError): + call_command('merge_attributes', + stdout=stdout, stderr=stderr, **source_has_descendants) + + source_does_not_exist = { + 'source': 'http://uri-does-not-exist-1.com', + 'target': first_leaf_attribute + } + stdout, stderr = io.StringIO(), io.StringIO() + with pytest.raises(CommandError): + call_command('merge_attributes', + stdout=stdout, stderr=stderr, **source_does_not_exist) + + target_does_not_exist = { + 'source': first_leaf_attribute, + 'target': 'http://uri-does-not-exist-1.com', + } + stdout, stderr = io.StringIO(), io.StringIO() + with pytest.raises(CommandError): + call_command('merge_attributes', + stdout=stdout, stderr=stderr, **target_does_not_exist) + + +@pytest.mark.parametrize('uri_prefix', NEW_MERGE_URI_PREFIXES) +@pytest.mark.usefixtures("_create_new_merge_attributes_and_views") +def test_that_the_freshly_created_merge_attributes_are_present(db, uri_prefix): + merge_attributes_uris = Attribute.objects.filter( + uri_prefix=uri_prefix).all().values_list( + 'uri', flat=True).distinct() + assert len(merge_attributes_uris) > 2 + unique_uri_prefixes = set(Attribute.objects.values_list("uri_prefix", flat=True)) + # test that the currently selected uri_prefix is in db + assert uri_prefix in unique_uri_prefixes + + for attribute_uri in merge_attributes_uris: + attribute = Attribute.objects.get(uri=attribute_uri) + original_attribute = Attribute.objects.get(uri_prefix=EXAMPLE_URI_PREFIX, path=attribute.path) + original_models_qs = [_get_queryset(i, attribute=original_attribute) for i in ATTRIBUTE_RELATED_MODELS_FIELDS] + if not any(len(i) > 0 for i in original_models_qs): + continue # skip this attribute + models_qs = [_get_queryset(i, attribute=attribute) for i in ATTRIBUTE_RELATED_MODELS_FIELDS] + assert any(len(i) > 0 for i in models_qs) + + # assert new views created by create_copy_of_view_that_uses_new_attribute + # foo-merge + foo_merge_view_qs = View.objects.filter(template__contains=FOO_MERGE_URI_PREFIX).exclude( + template__contains=BAR_MERGE_URI_PREFIX) + assert foo_merge_view_qs.count() == 1 + assert foo_merge_view_qs.first().uri_prefix == FOO_MERGE_URI_PREFIX + # bar-merge + bar_merge_view_qs = View.objects.filter(template__contains=BAR_MERGE_URI_PREFIX).exclude( + template__contains=FOO_MERGE_URI_PREFIX) + assert bar_merge_view_qs.count() == 1 + assert bar_merge_view_qs.first().uri_prefix == BAR_MERGE_URI_PREFIX + + +@pytest.mark.parametrize('source_uri_prefix', NEW_MERGE_URI_PREFIXES) +@pytest.mark.parametrize('save', [False, True]) +@pytest.mark.parametrize('delete', [False, True]) +@pytest.mark.parametrize('view', [False, True]) +@pytest.mark.usefixtures("_create_new_merge_attributes_and_views") +def test_command_merge_attributes(db, settings, source_uri_prefix, save, delete, view): + source_attribute_uris = Attribute.objects.filter( + uri_prefix=source_uri_prefix).all().values_list( + 'uri', flat=True).distinct() + assert len(source_attribute_uris) > 2 + unique_uri_prefixes = set(Attribute.objects.values_list("uri_prefix", flat=True)) + # test that the currently selected uri_prefix is in db + assert source_uri_prefix in unique_uri_prefixes + + for source_attribute_uri in source_attribute_uris: + source_attribute = Attribute.objects.get(uri=source_attribute_uri) + target_attribute = Attribute.objects.get(uri_prefix=EXAMPLE_URI_PREFIX, path=source_attribute.path) + before_source_related_qs = get_related_affected_instances(source_attribute) + # before_target_related_qs = get_related_affected_instances(target_attribute) + + command_kwargs = {'source': source_attribute.uri, + 'target': target_attribute.uri, + 'save': save, 'delete': delete, 'view': view} + failed = False + + if source_attribute.get_descendants(): + stdout, stderr = io.StringIO(), io.StringIO() + with pytest.raises(CommandError): + call_command('merge_attributes', + stdout=stdout, stderr=stderr, **command_kwargs) + failed = True + else: + stdout, stderr = io.StringIO(), io.StringIO() + call_command('merge_attributes', + stdout=stdout, stderr=stderr, **command_kwargs) + + if delete and not failed: + # assert that the source attribute was deleted + with pytest.raises(Attribute.DoesNotExist): + Attribute.objects.get(id=source_attribute.id) + else: + assert Attribute.objects.get(id=source_attribute.id) + + after_source_related_qs = get_related_affected_instances(source_attribute) + after_target_related_qs = get_related_affected_instances(target_attribute) + + if save and not failed: + + if any(i.exists() for i in before_source_related_qs): + assert not any(i.exists() for i in after_source_related_qs) + assert any(i.exists() for i in after_target_related_qs) + else: + if any(i.exists() for i in before_source_related_qs): + assert any(i.exists() for i in after_source_related_qs) + + +@pytest.mark.parametrize('source_uri_prefix', NEW_MERGE_URI_PREFIXES) +@pytest.mark.parametrize('save', [False, True]) +@pytest.mark.parametrize('delete', [False, True]) +@pytest.mark.parametrize('view', [False, True]) +@pytest.mark.usefixtures("_create_new_merge_attributes_and_views") +def test_command_merge_attributes_for_views(db, settings, source_uri_prefix, save, delete, view): + source_attributes = Attribute.objects.filter(uri_prefix=source_uri_prefix).all() + assert len(source_attributes) > 2 + unique_uri_prefixes = set(Attribute.objects.values_list("uri_prefix", flat=True)) + # test that the currently selected uri_prefix is in db + assert source_uri_prefix in unique_uri_prefixes + source_attribute_uri = Attribute.build_uri(uri_prefix=source_uri_prefix, path=VIEW_TEMPLATE_URI_PATH) + target_attribute_uri = Attribute.build_uri(uri_prefix=EXAMPLE_URI_PREFIX, path=VIEW_TEMPLATE_URI_PATH) + + stdout, stderr = io.StringIO(), io.StringIO() + before_source_related_view_uri_qs = View.objects.filter(**{"template__contains": f"'{source_attribute_uri}'"}).all() + + before_source_related_view_uri_templates = {i.uri: i.template for i in before_source_related_view_uri_qs} + + command_kwargs = {'source': source_attribute_uri, + 'target': target_attribute_uri, + 'save': save, 'delete': delete, 'view': view} + failed = False + call_command('merge_attributes', + stdout=stdout, stderr=stderr, **command_kwargs) + + if delete and not failed: + # assert that the source attribute was deleted + with pytest.raises(Attribute.DoesNotExist): + Attribute.objects.get(uri=source_attribute_uri) + else: + assert Attribute.objects.get(uri=source_attribute_uri) + + after_source_related_view_uri_qs = View.objects.filter(**{"template__contains": f"'{source_attribute_uri}'"}) + after_target_related_view_uri_qs = View.objects.filter(**{"template__contains": f"'{target_attribute_uri}'"}) + + if not save or not view: + assert not after_target_related_view_uri_qs.exists() + return + + if save and not failed: + pass + + if (save and not failed and view + and VIEW_TEMPLATE_URI_PATH in source_attribute_uri): + # assert that the attribute in the view template was replaced as well + # the EXAMPLE_VIEW_URI is from the target attribute + # uri_prefix = source_uri_prefix, uri_path = EXAMPLE_VIEW_URI_PATH + assert not after_source_related_view_uri_qs.exists() + assert after_target_related_view_uri_qs.exists() + + if (before_source_related_view_uri_templates and + source_attribute_uri != target_attribute_uri): + after_occurrences = list(filter(lambda x: target_attribute_uri in x, + after_target_related_view_uri_qs.first().template.splitlines())) + assert len(after_occurrences) == 1 + before_occurrences = list(filter(lambda x: target_attribute_uri in x, + before_source_related_view_uri_qs.first().template.splitlines())) + assert len(before_occurrences) == 0 diff --git a/rdmo/management/tests/test_viewset_import.py b/rdmo/management/tests/test_viewset_import.py index 23cb6742d5..7bf246b932 100644 --- a/rdmo/management/tests/test_viewset_import.py +++ b/rdmo/management/tests/test_viewset_import.py @@ -4,6 +4,8 @@ from rdmo.questions.models import Catalog, Page, Question, QuestionSet, Section +from .helpers_models import delete_all_objects + users = ( ('editor', 'editor'), ('reviewer', 'reviewer'), @@ -40,11 +42,7 @@ def test_list(db, client, username, password): @pytest.mark.parametrize('username,password', users) def test_create_create(db, client, username, password, json_data): - Catalog.objects.all().delete() - Section.objects.all().delete() - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) client.login(username=username, password=password) diff --git a/rdmo/management/tests/test_viewset_import_multisite.py b/rdmo/management/tests/test_viewset_import_multisite.py index 5a0e2f3d35..afb48bd5d7 100644 --- a/rdmo/management/tests/test_viewset_import_multisite.py +++ b/rdmo/management/tests/test_viewset_import_multisite.py @@ -2,10 +2,12 @@ from django.urls import reverse -from rdmo.core.tests import get_obj_perms_status_code -from rdmo.core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code from rdmo.questions.models import Catalog, Page, Question, QuestionSet, Section +from .helpers_models import delete_all_objects + status_map = { 'list': { 'default': 405, 'anonymous': 401 @@ -41,11 +43,7 @@ def test_list(db, client, username, password): @pytest.mark.parametrize('username,password', users) def test_create_create(db, client, username, password, json_data): - Catalog.objects.all().delete() - Section.objects.all().delete() - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) client.login(username=username, password=password) diff --git a/rdmo/management/tests/test_viewset_upload.py b/rdmo/management/tests/test_viewset_upload.py index 3715a44e0f..ac6606335a 100644 --- a/rdmo/management/tests/test_viewset_upload.py +++ b/rdmo/management/tests/test_viewset_upload.py @@ -7,6 +7,9 @@ from rdmo.questions.models import Catalog, Page, Question, QuestionSet, Section +from .helpers_models import delete_all_objects +from .helpers_xml import xml_error_files + users = ( ('editor', 'editor'), ('reviewer', 'reviewer'), @@ -32,6 +35,7 @@ } + @pytest.mark.parametrize('username,password', users) def test_list(db, client, username, password): client.login(username=username, password=password) @@ -54,16 +58,15 @@ def test_create(db, client, username, password): assert response.status_code == status_map['create'][username], response.json() if response.status_code == 200: for element in response.json(): - assert element.get('updated') is False + if username in ['api', 'editor']: + assert element.get('updated') is True + else: + assert element.get('updated') is False @pytest.mark.parametrize('username,password', users) def test_create_import_create(db, client, username, password): - Catalog.objects.all().delete() - Section.objects.all().delete() - Page.objects.all().delete() - QuestionSet.objects.all().delete() - Question.objects.all().delete() + delete_all_objects([Catalog, Section, Page, QuestionSet, Question]) client.login(username=username, password=password) @@ -107,13 +110,20 @@ def test_create_empty(db, client, username, password): @pytest.mark.parametrize('username,password', users) -def test_create_error(db, client, username, password): +@pytest.mark.parametrize('xml_file_path,error_message', xml_error_files.items()) +def test_create_error(db, client, username, password, xml_file_path, error_message): client.login(username=username, password=password) - xml_file = Path(settings.BASE_DIR) / 'xml' / 'error.xml' - + xml_file = Path(settings.BASE_DIR).joinpath(xml_file_path) url = reverse(urlnames['list']) - with open(xml_file, encoding='utf8') as f: - response = client.post(url, {'file': f}) + try: + with open(xml_file, encoding='utf8') as f: + response = client.post(url, {'file': f}) + except FileNotFoundError: + # one test case is for a non-existent file + response = client.post(url) assert response.status_code == status_map['create_error'][username], response.json() + if response.status_code == 400: + response_msg = " ".join(response.json()['file']) + assert error_message in response_msg diff --git a/rdmo/management/utils.py b/rdmo/management/utils.py new file mode 100644 index 0000000000..cff679811b --- /dev/null +++ b/rdmo/management/utils.py @@ -0,0 +1,9 @@ + +def replace_uri_in_template_string(template: str, source_uri: str, target_uri: str) -> str: + replacements = [ + (f"'{source_uri}'", f"'{target_uri}'"), + (f'"{source_uri}"', f'"{target_uri}"'), + ] + for pattern, replacement in replacements: + template = template.replace(pattern, replacement) + return template diff --git a/rdmo/management/views.py b/rdmo/management/views.py index c4bbd062a7..82a9bc1a28 100644 --- a/rdmo/management/views.py +++ b/rdmo/management/views.py @@ -1,4 +1,3 @@ -import hashlib import logging from django.contrib.auth.mixins import LoginRequiredMixin @@ -7,22 +6,15 @@ from rules import test_rule from rules.contrib.views import PermissionRequiredMixin as RulesPermissionRequiredMixin -from rdmo.core.views import CSRFViewMixin, PermissionRedirectMixin +from rdmo.core.views import CSRFViewMixin, PermissionRedirectMixin, StoreIdViewMixin logger = logging.getLogger(__name__) class ManagementView(LoginRequiredMixin, PermissionRedirectMixin, RulesPermissionRequiredMixin, - CSRFViewMixin, TemplateView): + CSRFViewMixin, StoreIdViewMixin, TemplateView): template_name = 'management/management.html' def has_permission(self): # Use test_rule from rules for permissions check return test_rule('management.can_view_management', self.request.user, self.request.site) - - def render_to_response(self, context, **response_kwargs): - storeid = hashlib.sha256(self.request.session.session_key.encode()).hexdigest() - - response = super().render_to_response(context, **response_kwargs) - response.set_cookie('storeid', storeid) - return response diff --git a/rdmo/management/viewsets.py b/rdmo/management/viewsets.py index 36c625dc85..2b5ec2d295 100644 --- a/rdmo/management/viewsets.py +++ b/rdmo/management/viewsets.py @@ -1,45 +1,32 @@ import logging +from django.contrib.sites.shortcuts import get_current_site from django.utils.translation import gettext_lazy as _ from rest_framework import viewsets +from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.serializers import ValidationError -from rdmo.conditions.models import Condition from rdmo.core.imports import handle_uploaded_file +from rdmo.core.permissions import CanToggleElementCurrentSite from rdmo.core.utils import get_model_field_meta, is_truthy -from rdmo.core.xml import convert_elements, flat_xml_to_elements, order_elements, read_xml_file -from rdmo.domain.models import Attribute -from rdmo.options.models import Option, OptionSet -from rdmo.questions.models import Catalog, Page, Question, QuestionSet, Section -from rdmo.tasks.models import Task -from rdmo.views.models import View +from rdmo.core.xml import parse_xml_to_elements +from .constants import RDMO_MODEL_PATH_MAPPER from .imports import import_elements logger = logging.getLogger(__name__) + class MetaViewSet(viewsets.ViewSet): permission_classes = (IsAuthenticated, ) def list(self, request, *args, **kwargs): - return Response({ - 'conditions.condition': get_model_field_meta(Condition), - 'domain.attribute': get_model_field_meta(Attribute), - 'options.optionset': get_model_field_meta(OptionSet), - 'options.option': get_model_field_meta(Option), - 'questions.catalog': get_model_field_meta(Catalog), - 'questions.section': get_model_field_meta(Section), - 'questions.page': get_model_field_meta(Page), - 'questions.questionset': get_model_field_meta(QuestionSet), - 'questions.question': get_model_field_meta(Question), - 'tasks.task': get_model_field_meta(Task), - 'views.view': get_model_field_meta(View) - }) + return Response({k: get_model_field_meta(val) for k, val in RDMO_MODEL_PATH_MAPPER.items()}) class UploadViewSet(viewsets.ViewSet): @@ -54,42 +41,27 @@ def create(self, request, *args, **kwargs): raise ValidationError({'file': [_('This field may not be blank.')]}) from e else: import_tmpfile_name = handle_uploaded_file(uploaded_file) - - # step 2: parse xml - root = read_xml_file(import_tmpfile_name) - if root is None: - logger.info('XML parsing error. Import failed.') - raise ValidationError({'file': [ - _('The content of the xml file does not consist of well formed data or markup.') - ]}) - - # step 3: create element dicts from xml try: - elements = flat_xml_to_elements(root) - except KeyError as e: - logger.info('Import failed with KeyError (%s)' % e) - raise ValidationError({'file': [_('This is not a valid RDMO XML file.')]}) from e - except TypeError as e: - logger.info('Import failed with TypeError (%s)' % e) - raise ValidationError({'file': [_('This is not a valid RDMO XML file.')]}) from e - except AttributeError as e: - logger.info('Import failed with AttributeError (%s)' % e) - raise ValidationError({'file': [_('This is not a valid RDMO XML file.')]}) from e - - # step 4: convert elements from previous versions - elements = convert_elements(elements, root.attrib.get('version')) - - # step 5: order the elements and return - elements = order_elements(elements) - - # step 6: convert elements to a list - elements = elements.values() + # step 1.1: initialize parse_xml_to_elements + # step 2-6: parse xml, validate and convert to + xml_parsed_elements, errors = parse_xml_to_elements(xml_file=import_tmpfile_name) + except ValidationError as e: + logger.info('Import failed with XML parsing errors.') + raise ValidationError({'file': e}) from e + + # step 7: check if valid + if errors: + _str_errors = ", ".join(map(str, errors)) + logger.info('Import failed with XML validation errors. %s', _str_errors) + raise ValidationError({'file': errors}) # step 8: import the elements if save=True is set - import_elements(elements, save=is_truthy(request.POST.get('import')), user=request.user) + imported_elements = import_elements(xml_parsed_elements, + save=is_truthy(request.POST.get('import')), + request=request) - # step 9: return the list of elements - return Response(elements) + # step 9: return the list of, json-serializable, elements + return Response(imported_elements) class ImportViewSet(viewsets.ViewSet): @@ -99,14 +71,31 @@ class ImportViewSet(viewsets.ViewSet): def create(self, request, *args, **kwargs): # step 1: store xml file as tmp file try: - elements = request.data['elements'] + elements_data = request.data['elements'] + elements = {i['uri']: i for i in elements_data if 'uri' in i} except KeyError as e: raise ValidationError({'elements': [_('This field may not be blank.')]}) from e except TypeError as e: raise ValidationError({'elements': [_('This is not a valid RDMO import JSON.')]}) from e # step 3: import the elements - import_elements(elements, user=request.user) + imported_elements = import_elements(elements, request=request) # step 4: return the list of elements - return Response(elements) + return Response(imported_elements) + + +class ElementToggleCurrentSiteViewSetMixin: + + @action(detail=True, methods=['put'], url_path="toggle-site", permission_classes=[CanToggleElementCurrentSite]) + def toggle_site(self, request, pk=None): + obj = self.get_object() + current_site = get_current_site(request) + has_current_site = obj.sites.filter(id=current_site.id).exists() + if has_current_site: + obj.sites.remove(current_site) + else: + obj.sites.add(current_site) + # need to return obj element for ElementSuccess reducer? + serializer = self.serializer_class(obj, context={'request': request}) + return Response(serializer.data) diff --git a/rdmo/options/imports.py b/rdmo/options/imports.py index 9823b98944..e39354f33a 100644 --- a/rdmo/options/imports.py +++ b/rdmo/options/imports.py @@ -1,16 +1,4 @@ -import logging - -from django.contrib.sites.models import Site - -from rdmo.core.imports import ( - check_permissions, - set_common_fields, - set_lang_field, - set_m2m_instances, - set_m2m_through_instances, - set_reverse_m2m_through_instance, - validate_instance, -) +from rdmo.core.import_helpers import ElementImportHelper, ExtraFieldHelper, ThroughInstanceMapper from .models import Option, OptionSet from .validators import ( @@ -20,68 +8,37 @@ OptionUniqueURIValidator, ) -logger = logging.getLogger(__name__) - - -def import_optionset(element, save=False, user=None): - try: - optionset = OptionSet.objects.get(uri=element.get('uri')) - except OptionSet.DoesNotExist: - optionset = OptionSet() - - set_common_fields(optionset, element) - - optionset.order = element.get('order') or 0 - optionset.provider_key = element.get('provider_key') or '' - - validate_instance(optionset, element, OptionSetLockedValidator, OptionSetUniqueURIValidator) - - check_permissions(optionset, element, user) - - if save and not element.get('errors'): - if optionset.id: - element['updated'] = True - logger.info('OptionSet %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('OptionSet created with uri %s.', element.get('uri')) - - optionset.save() - set_m2m_instances(optionset, 'conditions', element) - set_m2m_through_instances(optionset, 'options', element, 'optionset', 'option', 'optionset_options') - optionset.editors.add(Site.objects.get_current()) - - return optionset - - -def import_option(element, save=False, user=None): - try: - option = Option.objects.get(uri=element.get('uri')) - except Option.DoesNotExist: - option = Option() - - set_common_fields(option, element) - - option.additional_input = element.get('additional_input') or '' - - set_lang_field(option, 'text', element) - set_lang_field(option, 'help', element) - set_lang_field(option, 'view_text', element) - - validate_instance(option, element, OptionLockedValidator, OptionUniqueURIValidator) - - check_permissions(option, element, user) - - if save and not element.get('errors'): - if option.id: - element['updated'] = True - logger.info('Option %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('Option created with uri %s.', element.get('uri')) - - option.save() - set_reverse_m2m_through_instance(option, 'optionset', element, 'option', 'optionset', 'option_optionsets') - option.editors.add(Site.objects.get_current()) +import_helper_optionset = ElementImportHelper( + model=OptionSet, + validators=(OptionSetLockedValidator, OptionSetUniqueURIValidator), + extra_fields=( + ExtraFieldHelper(field_name='order'), + ExtraFieldHelper(field_name='provider_key', value=''), + ), + m2m_instance_fields=('conditions', ), + m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='options', + source_name='optionset', + target_name='option', + through_name='optionset_options' + ), + ] +) - return option +import_helper_option = ElementImportHelper( + model=Option, + validators=(OptionLockedValidator, OptionUniqueURIValidator), + lang_fields=('text', 'help', 'view_text'), + extra_fields=( + ExtraFieldHelper(field_name='additional_input', value=Option.ADDITIONAL_INPUT_NONE), + ), + reverse_m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='optionset', + source_name='option', + target_name='optionset', + through_name='option_optionsets' + ), + ] +) diff --git a/rdmo/options/providers.py b/rdmo/options/providers.py index 9a36d2af48..cc1c6bebe3 100644 --- a/rdmo/options/providers.py +++ b/rdmo/options/providers.py @@ -9,13 +9,13 @@ class Provider(Plugin): # determines if the page needs to be refreshed after a value is stored refresh = False - def get_options(self, project, search=None): + def get_options(self, project, search=None, user=None, site=None): raise NotImplementedError class SimpleProvider(Provider): - def get_options(self, project, search=None): + def get_options(self, project, search=None, user=None, site=None): return [ { 'id': 'simple_1', diff --git a/rdmo/options/renderers/mixins.py b/rdmo/options/renderers/mixins.py index 41ff1a6b58..49bf62610e 100644 --- a/rdmo/options/renderers/mixins.py +++ b/rdmo/options/renderers/mixins.py @@ -48,9 +48,9 @@ def render_option(self, xml, option): self.render_text_element(xml, 'dc:comment', {}, option['comment']) for lang_code, lang_string, lang_field in get_languages(): - self.render_text_element(xml, 'text', {'lang': lang_code}, option['text_%s' % lang_code]) - self.render_text_element(xml, 'help', {'lang': lang_code}, option['help_%s' % lang_code]) - self.render_text_element(xml, 'view_text', {'lang': lang_code}, option['view_text_%s' % lang_code]) + self.render_text_element(xml, 'text', {'lang': lang_code}, option[f'text_{lang_code}']) + self.render_text_element(xml, 'help', {'lang': lang_code}, option[f'help_{lang_code}']) + self.render_text_element(xml, 'view_text', {'lang': lang_code}, option[f'view_text_{lang_code}']) self.render_text_element(xml, 'additional_input', {}, option['additional_input']) xml.endElement('option') diff --git a/rdmo/options/serializers/v1/option.py b/rdmo/options/serializers/v1/option.py index 1bc0e2f919..5c07c4d8ef 100644 --- a/rdmo/options/serializers/v1/option.py +++ b/rdmo/options/serializers/v1/option.py @@ -3,6 +3,7 @@ from rdmo.core.serializers import ( ElementModelSerializerMixin, ElementWarningSerializerMixin, + MarkdownSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, ThroughModelSerializerMixin, TranslationSerializerMixin, @@ -14,7 +15,10 @@ class OptionSerializer(ThroughModelSerializerMixin, TranslationSerializerMixin, ElementModelSerializerMixin, ElementWarningSerializerMixin, - ReadOnlyObjectPermissionSerializerMixin, serializers.ModelSerializer): + ReadOnlyObjectPermissionSerializerMixin, MarkdownSerializerMixin, + serializers.ModelSerializer): + + markdown_fields = ('text', 'help') model = serializers.SerializerMethodField() uri_path = serializers.CharField(required=True) diff --git a/rdmo/options/serializers/v1/optionset.py b/rdmo/options/serializers/v1/optionset.py index 264c2f7128..5f9ee6cb66 100644 --- a/rdmo/options/serializers/v1/optionset.py +++ b/rdmo/options/serializers/v1/optionset.py @@ -33,6 +33,8 @@ class OptionSetSerializer(ThroughModelSerializerMixin, ElementModelSerializerMix read_only = serializers.SerializerMethodField() + condition_uris = serializers.SerializerMethodField() + class Meta: model = OptionSet fields = ( @@ -51,6 +53,7 @@ class Meta: 'questions', 'editors', 'read_only', + 'condition_uris', ) through_fields = ( ('options', 'optionset', 'option', 'optionset_options'), @@ -60,6 +63,9 @@ class Meta: OptionSetLockedValidator() ) + def get_condition_uris(self, obj): + return [condition.uri for condition in obj.conditions.all()] + class OptionSetNestedSerializer(OptionSetSerializer): diff --git a/rdmo/options/tests/test_admin.py b/rdmo/options/tests/test_admin.py index 3a8cbd1b20..cc3aec9653 100644 --- a/rdmo/options/tests/test_admin.py +++ b/rdmo/options/tests/test_admin.py @@ -1,17 +1,13 @@ from django.urls import reverse -def test_optionset_search(db, client): - client.login(username='admin', password='admin') - +def test_optionset_search(admin_client): url = reverse('admin:options_optionset_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 -def test_option_search(db, client): - client.login(username='admin', password='admin') - +def test_option_search(admin_client): url = reverse('admin:options_option_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 diff --git a/rdmo/options/tests/test_viewset_options_multisite.py b/rdmo/options/tests/test_viewset_options_multisite.py index 9062de7f74..6834c6597e 100644 --- a/rdmo/options/tests/test_viewset_options_multisite.py +++ b/rdmo/options/tests/test_viewset_options_multisite.py @@ -4,9 +4,10 @@ from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import Option from .test_viewset_options import urlnames diff --git a/rdmo/options/tests/test_viewset_optionsets_multisite.py b/rdmo/options/tests/test_viewset_optionsets_multisite.py index 2c74bd8e79..55bd2a30eb 100644 --- a/rdmo/options/tests/test_viewset_optionsets_multisite.py +++ b/rdmo/options/tests/test_viewset_optionsets_multisite.py @@ -4,9 +4,10 @@ from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import OptionSet from .test_viewset_optionsets import urlnames diff --git a/rdmo/projects/admin.py b/rdmo/projects/admin.py index fcf6749b5c..1c82a6a5e8 100644 --- a/rdmo/projects/admin.py +++ b/rdmo/projects/admin.py @@ -1,5 +1,7 @@ from django.contrib import admin from django.db.models import Prefetch +from django.urls import reverse +from django.utils.safestring import mark_safe from .models import ( Continuation, @@ -30,7 +32,7 @@ def get_queryset(self, request): ) def owners(self, obj): - return ', '.join([membership.user.username for membership in obj.owner_memberships]) + return [membership.user.get_full_name() for membership in obj.owner_memberships] @admin.register(Membership) @@ -80,7 +82,7 @@ class IssueResourceAdmin(admin.ModelAdmin): @admin.register(Snapshot) class SnapshotAdmin(admin.ModelAdmin): search_fields = ('title', 'project__title', 'project__user__username') - list_display = ('title', 'project', 'owners', 'updated', 'created') + list_display = ('title', 'project_title', 'project_owners', 'updated', 'created') def get_queryset(self, request): return Snapshot.objects.prefetch_related( @@ -91,16 +93,39 @@ def get_queryset(self, request): ) ).select_related('project') - def owners(self, obj): - return ', '.join([membership.user.username for membership in obj.project.owner_memberships]) + def project_title(self, obj): + url = reverse('admin:projects_project_change', args=[obj.project.id]) + link = f'<a href="{url}">{obj.project.title}</a>' + return mark_safe(link) + + def project_owners(self, obj): + return [membership.user.get_full_name() for membership in obj.project.owner_memberships] @admin.register(Value) class ValueAdmin(admin.ModelAdmin): search_fields = ('attribute__uri', 'project__title', 'snapshot__title', 'project__user__username') - list_display = ('attribute', 'set_prefix', 'set_index', 'collection_index', 'project', 'snapshot_title') + list_display = ('attribute', 'set_prefix', 'set_index', 'collection_index', 'value_type', + 'project_title', 'project_owners', 'snapshot_title', 'updated', 'created') list_filter = ('value_type', ) + def get_queryset(self, request): + return Value.objects.prefetch_related( + Prefetch( + 'project__memberships', + queryset=Membership.objects.filter(role='owner').select_related('user'), + to_attr='owner_memberships' + ) + ).select_related('attribute', 'project', 'snapshot') + def snapshot_title(self, obj): if obj.snapshot: return obj.snapshot.title + + def project_title(self, obj): + url = reverse('admin:projects_project_change', args=[obj.project.id]) + link = f'<a href="{url}">{obj.project.title}</a>' + return mark_safe(link) + + def project_owners(self, obj): + return [membership.user.get_full_name() for membership in obj.project.owner_memberships] diff --git a/rdmo/projects/assets/js/actions/actionTypes.js b/rdmo/projects/assets/js/actions/actionTypes.js new file mode 100644 index 0000000000..c879bf0e62 --- /dev/null +++ b/rdmo/projects/assets/js/actions/actionTypes.js @@ -0,0 +1,23 @@ +export const DELETE_CONFIG = 'config/deleteConfig' +export const FETCH_CURRENT_USER_ERROR = 'currentUser/fetchCurrentUserError' +export const FETCH_CURRENT_USER_INIT = 'currentUser/fetchCurrentUserInit' +export const FETCH_CURRENT_USER_SUCCESS = 'currentUser/fetchCurrentUserSuccess' +export const FETCH_PROJECTS_ERROR = 'projects/fetchProjectsError' +export const FETCH_PROJECTS_INIT = 'projects/fetchProjectsInit' +export const FETCH_PROJECTS_SUCCESS = 'projects/fetchProjectsSuccess' +export const FETCH_INVITATIONS_ERROR = 'projects/fetchInvitationsError' +export const FETCH_INVITATIONS_INIT = 'projects/fetchInvitationsInit' +export const FETCH_INVITATIONS_SUCCESS = 'projects/fetchInvitationsSuccess' +export const FETCH_CATALOGS_ERROR = 'projects/fetchCatalogsError' +export const FETCH_CATALOGS_INIT = 'projects/fetchCatalogsInit' +export const FETCH_CATALOGS_SUCCESS = 'projects/fetchCatalogsSuccess' +export const UPDATE_CONFIG = 'config/updateConfig' +export const UPLOAD_PROJECT_ERROR = 'import/uploadProjectError' +export const UPLOAD_PROJECT_INIT = 'import/uploadProjectInit' +export const UPLOAD_PROJECT_SUCCESS = 'import/uploadProjectSuccess' +export const FETCH_FILETYPES_ERROR = 'projects/fetchFileTypesError' +export const FETCH_FILETYPES_INIT = 'projects/fetchFileTypesInit' +export const FETCH_FILETYPES_SUCCESS = 'projects/fetchFileTypesSuccess' +export const FETCH_IMPORT_URLS_ERROR = 'projects/fetchImportUrlsError' +export const FETCH_IMPORT_URLS_INIT = 'projects/fetchImportUrlsInit' +export const FETCH_IMPORT_URLS_SUCCESS = 'projects/fetchImportUrlsSuccess' diff --git a/rdmo/projects/assets/js/actions/configActions.js b/rdmo/projects/assets/js/actions/configActions.js new file mode 100644 index 0000000000..11da6d3f84 --- /dev/null +++ b/rdmo/projects/assets/js/actions/configActions.js @@ -0,0 +1,9 @@ +import { DELETE_CONFIG, UPDATE_CONFIG } from './actionTypes' + +export function updateConfig(path, value) { + return {type: UPDATE_CONFIG, path, value} +} + +export function deleteConfig(path) { + return {type: DELETE_CONFIG, path} +} diff --git a/rdmo/projects/assets/js/actions/projectsActions.js b/rdmo/projects/assets/js/actions/projectsActions.js new file mode 100644 index 0000000000..d8529cb539 --- /dev/null +++ b/rdmo/projects/assets/js/actions/projectsActions.js @@ -0,0 +1,165 @@ +import ProjectsApi from '../api/ProjectsApi' +import { FETCH_PROJECTS_ERROR, FETCH_PROJECTS_INIT, FETCH_PROJECTS_SUCCESS, + FETCH_INVITATIONS_ERROR, FETCH_INVITATIONS_INIT, FETCH_INVITATIONS_SUCCESS, + FETCH_CATALOGS_ERROR, FETCH_CATALOGS_INIT, FETCH_CATALOGS_SUCCESS, + FETCH_FILETYPES_ERROR, FETCH_FILETYPES_INIT, FETCH_FILETYPES_SUCCESS, + FETCH_IMPORT_URLS_ERROR, FETCH_IMPORT_URLS_INIT, FETCH_IMPORT_URLS_SUCCESS, + UPLOAD_PROJECT_ERROR, UPLOAD_PROJECT_INIT, UPLOAD_PROJECT_SUCCESS } + from './actionTypes' + +import * as configActions from './configActions' + +export function fetchProjects(pageReset = true) { + return function(dispatch, getState) { + if (pageReset === true) { + dispatch(configActions.updateConfig('params.page', '1')) + } + const params = getState().config.params + dispatch(fetchProjectsInit()) + const action = (dispatch) => ProjectsApi.fetchProjects(params || {}) + .then(projects => { + dispatch(fetchProjectsSuccess(projects, !pageReset))}) + + return dispatch(action) + .catch(error => dispatch(fetchProjectsError(error))) + } +} + +export function fetchProjectsInit() { + return {type: FETCH_PROJECTS_INIT} +} + +export function fetchProjectsSuccess(projects, shouldConcatenate) { + return {type: FETCH_PROJECTS_SUCCESS, projects, shouldConcatenate} +} + +export function fetchProjectsError(error) { + return function(dispatch) { + if (error.constructor.name === 'BadRequestError' && error.errors.catalog) { + dispatch(configActions.deleteConfig('params.catalog')) + dispatch(fetchProjects()) + } else { + dispatch({type: FETCH_PROJECTS_ERROR, error}) + } + } +} + +export function fetchCatalogs() { + return function(dispatch) { + dispatch(fetchCatalogsInit()) + const action = (dispatch) => ProjectsApi.fetchCatalogs() + .then(catalogs => { + dispatch(fetchCatalogsSuccess({ catalogs }))}) + + return dispatch(action) + .catch(error => dispatch(fetchCatalogsError(error))) + } +} + +export function fetchCatalogsInit() { + return {type: FETCH_CATALOGS_INIT} +} + +export function fetchCatalogsSuccess(catalogs) { + return {type: FETCH_CATALOGS_SUCCESS, catalogs} +} + +export function fetchCatalogsError(error) { + return {type: FETCH_CATALOGS_ERROR, error} +} + +export function fetchAllowedFileTypes() { + return function(dispatch) { + dispatch(fetchAllowedFileTypesInit()) + const action = (dispatch) => ProjectsApi.fetchAllowedFileTypes() + .then(allowedTypes => { + dispatch(fetchAllowedFileTypesSuccess({ allowedTypes }))}) + + return dispatch(action) + .catch(error => dispatch(fetchAllowedFileTypesError(error))) + } +} + +export function fetchAllowedFileTypesInit() { + return {type: FETCH_FILETYPES_INIT} +} + +export function fetchAllowedFileTypesSuccess(allowedTypes) { + return {type: FETCH_FILETYPES_SUCCESS, allowedTypes} +} + +export function fetchAllowedFileTypesError(error) { + return {type: FETCH_FILETYPES_ERROR, error} +} + +export function fetchImportUrls() { + return function(dispatch) { + dispatch(fetchImportUrlsInit()) + const action = (dispatch) => ProjectsApi.fetchDirectImportUrls() + .then(importUrls => { + dispatch(fettchImportUrlsSuccess({ importUrls }))}) + + return dispatch(action) + .catch(error => dispatch(fetchImportUrlsError(error))) + } +} + +export function fetchImportUrlsInit() { + return {type: FETCH_IMPORT_URLS_INIT} +} + +export function fettchImportUrlsSuccess(importUrls) { + return {type: FETCH_IMPORT_URLS_SUCCESS, importUrls } +} + +export function fetchImportUrlsError(error) { + return {type: FETCH_IMPORT_URLS_ERROR, error} +} + +export function fetchInvitations() { + return function(dispatch) { + dispatch(fetchInvitationsInit()) + const action = (dispatch) => ProjectsApi.fetchInvites() + .then(invites => { + dispatch(fetchInvitationsSuccess({ invites }))}) + + return dispatch(action) + .catch(error => dispatch(fetchInvitationsError(error))) + } +} + +export function fetchInvitationsInit() { + return {type: FETCH_INVITATIONS_INIT} +} + +export function fetchInvitationsSuccess(invites) { + return {type: FETCH_INVITATIONS_SUCCESS, invites} +} + +export function fetchInvitationsError(error) { + return {type: FETCH_INVITATIONS_ERROR, error} +} + +export function uploadProject(url, file) { + return function(dispatch) { + dispatch(uploadProjectInit()) + + return ProjectsApi.uploadProject(url, file) + .then(project => dispatch(uploadProjectSuccess(project))) + .catch(error => { + dispatch(uploadProjectError(error)) + }) + } +} + +export function uploadProjectInit() { + return {type: UPLOAD_PROJECT_INIT} +} + +export function uploadProjectSuccess(project) { + return {type: UPLOAD_PROJECT_SUCCESS, project} +} + +export function uploadProjectError(error) { + return {type: UPLOAD_PROJECT_ERROR, error} +} diff --git a/rdmo/projects/assets/js/actions/userActions.js b/rdmo/projects/assets/js/actions/userActions.js new file mode 100644 index 0000000000..6e26ef7688 --- /dev/null +++ b/rdmo/projects/assets/js/actions/userActions.js @@ -0,0 +1,30 @@ +import AccountsApi from '../api/AccountsApi' +import { + FETCH_CURRENT_USER_ERROR, + FETCH_CURRENT_USER_INIT, + FETCH_CURRENT_USER_SUCCESS } + from './actionTypes' + +export function fetchCurrentUser() { + return function(dispatch) { + dispatch(fetchCurrentUserInit()) + const action = (dispatch) => AccountsApi.fetchCurrentUser(true) + .then(currentUser => { + dispatch(fetchCurrentUserSuccess({ currentUser }))}) + + return dispatch(action) + .catch(error => dispatch(fetchCurrentUserError(error))) + } +} + +export function fetchCurrentUserInit() { + return {type: FETCH_CURRENT_USER_INIT} +} + +export function fetchCurrentUserSuccess(currentUser) { + return {type: FETCH_CURRENT_USER_SUCCESS, currentUser} +} + +export function fetchCurrentUserError(error) { + return {type: FETCH_CURRENT_USER_ERROR, error} +} diff --git a/rdmo/projects/assets/js/api/AccountsApi.js b/rdmo/projects/assets/js/api/AccountsApi.js new file mode 100644 index 0000000000..94e42a94ad --- /dev/null +++ b/rdmo/projects/assets/js/api/AccountsApi.js @@ -0,0 +1,9 @@ +import BaseApi from 'rdmo/core/assets/js/api/BaseApi' + +class AccountsApi extends BaseApi { + static fetchCurrentUser() { + return this.get('/api/v1/accounts/users/current/') + } +} + +export default AccountsApi diff --git a/rdmo/projects/assets/js/api/ProjectsApi.js b/rdmo/projects/assets/js/api/ProjectsApi.js new file mode 100644 index 0000000000..5fc35590ad --- /dev/null +++ b/rdmo/projects/assets/js/api/ProjectsApi.js @@ -0,0 +1,119 @@ +import Cookies from 'js-cookie' +import BaseApi from 'rdmo/core/assets/js/api/BaseApi' +import { encodeParams } from 'rdmo/core/assets/js/utils/api' +import baseUrl from 'rdmo/core/assets/js/utils/baseUrl' + +function BadRequestError(errors) { + this.errors = errors +} + +class ProjectsApi extends BaseApi { + + static fetchProjects(params, fetchParams = {}) { + return fetch('/api/v1/projects/projects/?' + encodeParams(params), fetchParams).then(response => { + if (response.ok) { + return response.json() + } else if (response.status == 400) { + return response.json().then(errors => { + throw new BadRequestError(errors) + }) + } else { + throw new Error(response.statusText) + } + }) + } + + static fetchCatalogs() { + return fetch('/api/v1/projects/catalogs/').then(response => { + if (response.ok) { + return response.json() + } else { + throw new Error(response.statusText) + } + }) + } + + static fetchAllowedFileTypes() { + return fetch('/api/v1/projects/projects/upload-accept/') + .then(response => { + if (response.ok) { + return response.text() + .then(text => { + try { + // Attempt to parse the text as JSON + const jsonData = JSON.parse(text) + // Check if the parsed data is an array + if (Array.isArray(jsonData)) { + return jsonData + } + } catch (error) { + // If JSON.parse fails, handle text as plain string below + } + + // If it's not a JSON array, process it as a string + const cleanedText = text.replace(/^"|"$/g, '') + return cleanedText ? cleanedText.split(',') : [] + }) + } else { + throw new Error(response.statusText) + } + }) + } + + static fetchDirectImportUrls() { + return fetch('/api/v1/projects/projects/imports/') + .then(response => { + if (response.ok) { + return response.json() + } else { + throw new Error(response.statusText) + } + }) + } + + static fetchInvites() { + return fetch('/api/v1/projects/invites/user') + .then(response => { + if (response.ok) { + return response.json() + } else { + throw new Error(response.statusText) + } + }) + } + + static uploadProject(url, file) { + var formData = new FormData() + formData.append('method', 'upload_file') + formData.append('uploaded_file', file) + return fetch(baseUrl + url, { + method: 'POST', + headers: { + 'X-CSRFToken': Cookies.get('csrftoken'), + }, + body: formData + }).catch(error => { + throw new Error(`API error: ${error.message}`) + }).then(response => { + if (response.ok) { + if (response.url) { + // open in new window: + // window.open(response.url, '_blank') + // open in same window: + window.location.href = response.url + } else { + throw new Error('Response does not contain a URL') + } + } else if (response.status == 400) { + return response.json().then(errors => { + throw new Error(`Validation error: ${JSON.stringify(errors)}`) + }) + } else { + throw new Error(`API error: ${response.statusText} (${response.status})`) + } + }) + } + +} + +export default ProjectsApi diff --git a/rdmo/projects/assets/js/components/helper/PendingInvitations.js b/rdmo/projects/assets/js/components/helper/PendingInvitations.js new file mode 100644 index 0000000000..9290ccb32c --- /dev/null +++ b/rdmo/projects/assets/js/components/helper/PendingInvitations.js @@ -0,0 +1,35 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { ROLE_LABELS } from '../../utils' + +const PendingInvitations = ({ invitations }) => { + + return ( + invitations?.map(item => ( + <div key={item.id} className="row-container"> + <div className="w-100 mb-5"> + <b>{item.title}</b> + </div> + <div className="w-50"> + {ROLE_LABELS[item.role]} + </div> + <div className="w-50 align-right"> + <button className="btn btn-xs btn-success ml-10" onClick={() => { window.location.href = `/projects/join/${item.token}` }}>{gettext('Accept')}</button> + <button className="btn btn-xs btn-danger ml-10" onClick={() => { window.location.href = `/projects/cancel/${item.token}` }}>{gettext('Decline')}</button> + </div> + </div> + )) + ) +} + +PendingInvitations.propTypes = { + invitations: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + project: PropTypes.number.isRequired, + role: PropTypes.string.isRequired, + token: PropTypes.string.isRequired + })), +} + +export default PendingInvitations diff --git a/rdmo/projects/assets/js/components/helper/ProjectFilters.js b/rdmo/projects/assets/js/components/helper/ProjectFilters.js new file mode 100644 index 0000000000..5b317224f6 --- /dev/null +++ b/rdmo/projects/assets/js/components/helper/ProjectFilters.js @@ -0,0 +1,180 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { get } from 'lodash' +import DatePicker from 'react-datepicker' +import { formatISO, set } from 'date-fns' + +import { Link, Select } from 'rdmo/core/assets/js/components' +import useDatePicker from '../../hooks/useDatePicker' +import { language } from 'rdmo/core/assets/js/utils' + +const ProjectFilters = ({ catalogs, config, configActions, isManager, projectsActions }) => { + const { + dateRange, + dateFormat, + getLocale, + setStartDate, + setEndDate + } = useDatePicker() + + const showFilters = get(config, 'showFilters', false) + const toggleFilters = () => configActions.updateConfig('showFilters', !showFilters) + + const resetAllFilters = () => { + configActions.deleteConfig('params.catalog') + configActions.deleteConfig('params.created_after') + setStartDate('created', null) + console.log('dateRange', dateRange) + configActions.deleteConfig('params.created_before') + setEndDate('created', null) + configActions.deleteConfig('params.last_changed_after') + setStartDate('last_changed', null) + configActions.deleteConfig('params.last_changed_before') + setEndDate('last_changed', null) + configActions.updateConfig('params.page', '1') + projectsActions.fetchProjects() + } + + const catalogOptions = catalogs?.map(catalog => ({ value: catalog.id.toString(), label: catalog.title })) + const selectedCatalog = get(config, 'params.catalog', '') + const updateCatalogFilter = (value) => { + value ? configActions.updateConfig('params.catalog', value) : configActions.deleteConfig('params.catalog') + projectsActions.fetchProjects() + } + + // Abstract function to handle date change + const handleDateChange = (type, position, date) => { + if (position === 'start') { + setStartDate(type, date) + if (date) { + const startOfDayDate = set(date, { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }) + configActions.updateConfig(`params.${type}_after`, formatISO(startOfDayDate, { representation: 'complete' })) + } else { + configActions.deleteConfig(`params.${type}_after`) + } + } else if (position === 'end') { + setEndDate(type, date) + if (date) { + const endOfDayDate = set(date, { hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }) + configActions.updateConfig(`params.${type}_before`, formatISO(endOfDayDate, { representation: 'complete' })) + } else { + configActions.deleteConfig(`params.${type}_before`) + } + } + projectsActions.fetchProjects() + } + + return ( + <> + {showFilters && ( + <div className="panel panel-default panel-filters mt-10 mb-0"> + <div className="panel-body"> + <div className="row"> + <div className={`col-md-${isManager ? 4 : 8}`}> + <label className="control-label text-muted">{gettext('Filter by catalog')}</label> + <div className="search-container"> + <Select + className="select-custom" + onChange={updateCatalogFilter} + options={catalogOptions ?? []} + placeholder={gettext('Select catalog')} + value={selectedCatalog} + /> + </div> + </div> + {isManager && ( + <div className="col-md-4"> + <label className="control-label text-muted">{gettext('Filter by created date')}</label> + <div className="projects-datepicker"> + <div className="row"> + <div className="col-md-6"> + <DatePicker + autoComplete="off" + className="form-control" + dateFormat={dateFormat} + id="created-start-date-picker" + isClearable + locale={getLocale(language)} + onChange={date => handleDateChange('created', 'start', date)} + placeholderText={gettext('Select start date')} + selected={dateRange.createdStart ?? get(config, 'params.created_after', '')} + /> + </div> + <div className="col-md-6"> + <DatePicker + autoComplete="off" + className="form-control" + dateFormat={dateFormat} + id="created-end-date-picker" + isClearable + locale={getLocale(language)} + onChange={date => handleDateChange('created', 'end', date)} + placeholderText={gettext('Select end date')} + selected={dateRange.createdEnd ?? get(config, 'params.created_before', '')} + /> + </div> + </div> + </div> + </div> + )} + <div className="col-md-4"> + <label className="control-label text-muted">{gettext('Filter by last changed date')}</label> + <div className="projects-datepicker"> + <div className="row"> + <div className="col-md-6"> + <DatePicker + autoComplete="off" + className="form-control" + dateFormat={dateFormat} + id="last-changed-start-date-picker" + isClearable + locale={getLocale(language)} + onChange={date => handleDateChange('last_changed', 'start', date)} + placeholderText={gettext('Select start date')} + selected={dateRange.lastChangedStart ?? get(config, 'params.last_changed_after', '')} + /> + </div> + <div className="col-md-6"> + <DatePicker + autoComplete="off" + className="form-control" + dateFormat={dateFormat} + id="last-changed-end-date-picker" + isClearable + locale={getLocale(language)} + onChange={date => handleDateChange('last_changed', 'end', date)} + placeholderText={gettext('Select end date')} + selected={dateRange.lastChangedEnd ?? get(config, 'params.last_changed_before', '')} + /> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + )} + <div className="pull-right mt-5"> + {showFilters && !Object.keys(config.params).every(key => ['ordering', 'page', 'search', 'user'].includes(key)) && ( + <Link className="element-link mr-10 mb-10" onClick={resetAllFilters}> + {gettext('Reset all filters')} + </Link> + )} + <Link className="element-link mb-10" onClick={toggleFilters}> + {showFilters ? gettext('Hide filters') : gettext('Show filters')} + </Link> + </div> + </> + ) +} + +ProjectFilters.propTypes = { + catalogs: PropTypes.arrayOf(PropTypes.object).isRequired, + config: PropTypes.object.isRequired, + configActions: PropTypes.object.isRequired, + isManager: PropTypes.bool.isRequired, + projectsActions: PropTypes.object.isRequired, +} + +export default ProjectFilters diff --git a/rdmo/projects/assets/js/components/helper/ProjectImport.js b/rdmo/projects/assets/js/components/helper/ProjectImport.js new file mode 100644 index 0000000000..ddb741b4f4 --- /dev/null +++ b/rdmo/projects/assets/js/components/helper/ProjectImport.js @@ -0,0 +1,42 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { UploadDropZone } from 'rdmo/core/assets/js/components' + +const ProjectImport = ({ allowedTypes, handleImport, importUrls}) => { + + const renderDirectImportLinks = () => { + return ( + <div className="mt-10"> + <label className="control-label">{gettext('Import directly')}</label> + <ul className='list-unstyled mb-0'> + {importUrls.map((url) => ( + <li key={url.key}> + <a href={url.href} target='_blank' rel='noopener noreferrer'> + {url.label} + </a> + </li> + ))} + </ul> + </div> + ) + } + + return ( + <> + <label className="control-label">{gettext('Import from file')}</label> + <UploadDropZone + acceptedTypes={allowedTypes} + onImportFile={handleImport} /> + {importUrls.length > 0 && renderDirectImportLinks()} + </> + ) +} + +ProjectImport.propTypes = { + allowedTypes: PropTypes.arrayOf(PropTypes.string), + handleImport: PropTypes.func.isRequired, + importUrls: PropTypes.arrayOf(PropTypes.object).isRequired +} + +export default ProjectImport diff --git a/rdmo/projects/assets/js/components/helper/Table.js b/rdmo/projects/assets/js/components/helper/Table.js new file mode 100644 index 0000000000..db4f899b10 --- /dev/null +++ b/rdmo/projects/assets/js/components/helper/Table.js @@ -0,0 +1,126 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { get } from 'lodash' + +const Table = ({ + cellFormatters, + columnWidths, + config, + configActions, + data, + headerFormatters, + projectsActions, + sortableColumns, + /* order of elements in 'visibleColumns' corresponds to order of columns in table */ + visibleColumns, +}) => { + const extractSortingParams = (params) => { + const { ordering } = params || {} + + if (!ordering) { + return { sortOrder: undefined, sortColumn: undefined } + } + + const sortOrder = ordering.startsWith('-') ? 'desc' : 'asc' + const sortColumn = sortOrder === 'desc' ? ordering.substring(1) : ordering + + return { sortColumn, sortOrder } + } + + const params = get(config, 'params', {}) + const { sortColumn, sortOrder } = extractSortingParams(params) + + const handleHeaderClick = (column) => { + if (sortableColumns.includes(column)) { + if (sortColumn === column) { + if (sortOrder === 'asc') { + configActions.updateConfig('params.ordering', `-${column}`) + } else if (sortOrder === 'desc') { + configActions.deleteConfig('params.ordering') + } else { + configActions.updateConfig('params.ordering', column) + } + } else { + configActions.updateConfig('params.ordering', column) + } + projectsActions.fetchProjects() + } + } + + const renderSortIcon = (column) => { + const isSortColumn = sortColumn === column + const isAsc = sortOrder === 'asc' + + return ( + <span className="ml-5 sort-icon"> + <i className={`fa fa-sort${isSortColumn ? isAsc ? '-asc' : '-desc' : ''} ${isSortColumn ? '' : 'text-muted'}`} /> + </span> + ) + } + + const renderHeaders = () => { + return ( + <thead className="thead-dark"> + <tr> + {visibleColumns.map((column, index) => { + const headerFormatter = headerFormatters[column] + const columnHeaderContent = headerFormatter && headerFormatter.render ? headerFormatter.render(column) : column + + return ( + <th key={column} style={{ width: columnWidths[index] }} onClick={() => handleHeaderClick(column)}> + {columnHeaderContent} + {sortableColumns.includes(column) && renderSortIcon(column)} + </th> + ) + })} + </tr> + </thead> + ) + } + + const formatCellContent = (row, column, content) => { + if (cellFormatters && cellFormatters[column] && typeof cellFormatters[column] === 'function') { + return cellFormatters[column](content, row) + } + return content + } + + const renderRows = () => { + return ( + <tbody> + {data.map((row, index) => ( + <tr key={index}> + {visibleColumns.map((column, index) => ( + <td key={column} style={{ width: columnWidths[index] }}> + {formatCellContent(row, column, row[column])} + </td> + ))} + </tr> + ))} + </tbody> + ) + } + + return ( + <div id="projects-table" className="table-container"> + <table className="table table-borderless"> + {renderHeaders()} + {renderRows()} + </table> + </div> + ) +} + +Table.propTypes = { + cellFormatters: PropTypes.object, + columnWidths: PropTypes.arrayOf(PropTypes.string), + config: PropTypes.object, + configActions: PropTypes.object, + data: PropTypes.arrayOf(PropTypes.object).isRequired, + headerFormatters: PropTypes.object, + projectsActions: PropTypes.object, + sortableColumns: PropTypes.arrayOf(PropTypes.string), + visibleColumns: PropTypes.arrayOf(PropTypes.string), +} + +export default Table diff --git a/rdmo/projects/assets/js/components/helper/index.js b/rdmo/projects/assets/js/components/helper/index.js new file mode 100644 index 0000000000..95319b6cb1 --- /dev/null +++ b/rdmo/projects/assets/js/components/helper/index.js @@ -0,0 +1,4 @@ +export { default as PendingInvitations } from './PendingInvitations' +export { default as ProjectFilters } from './ProjectFilters' +export { default as ProjectImport } from './ProjectImport' +export { default as Table } from './Table' diff --git a/rdmo/projects/assets/js/components/main/Projects.js b/rdmo/projects/assets/js/components/main/Projects.js new file mode 100644 index 0000000000..aac7337816 --- /dev/null +++ b/rdmo/projects/assets/js/components/main/Projects.js @@ -0,0 +1,242 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { PendingInvitations, ProjectFilters, ProjectImport, Table } from '../helper' +import { Link, Modal, SearchField } from 'rdmo/core/assets/js/components' +import { useFormattedDateTime, useModal, useScrollToTop } from 'rdmo/core/assets/js/hooks' +import { language } from 'rdmo/core/assets/js/utils' +import { getTitlePath, getUserRoles, userIsManager, HEADER_FORMATTERS, SORTABLE_COLUMNS } from '../../utils' +import { get, isEmpty } from 'lodash' + +const Projects = ({ config, configActions, currentUserObject, projectsActions, projectsObject }) => { + const { allowedTypes, catalogs, importUrls, invites, projects, projectsCount, hasNext } = projectsObject + + const { currentUser } = currentUserObject + const { myProjects } = config + + const { showTopButton, scrollToTop } = useScrollToTop() + + const { show: showInvitations, open: openInvitations, close: closeInvitations } = useModal() + const { show: showImport, open: openImport, close: closeImport } = useModal() + + const invitationsModalProps = { + title: gettext('Pending invitations'), + show: showInvitations, + onClose: closeInvitations + } + + const importModalProps = { + title: gettext('Import project'), + show: showImport, + onClose: closeImport + } + + const displayMessage = interpolate(gettext('%s of %s projects are displayed'), [projects.length > projectsCount ? projectsCount : projects.length, projectsCount]) + + const getProgressString = (row) => { + return (row.progress_total ? interpolate(gettext('%s of %s'), [row.progress_count ?? 0, row.progress_total]) : null) + } + + const currentUserId = currentUser.id + const isManager = userIsManager(currentUser) + + const searchString = get(config, 'params.search', '') + const updateSearchString = (value) => { + value ? configActions.updateConfig('params.search', value) : configActions.deleteConfig('params.search') + } + + const viewLinkText = myProjects ? gettext('View all projects') : gettext('View my projects') + const headline = myProjects ? gettext('My projects') : gettext('All projects') + + const handleView = () => { + configActions.updateConfig('myProjects', !myProjects) + myProjects ? configActions.deleteConfig('params.user') : configActions.updateConfig('params.user', currentUserId) + projectsActions.fetchProjects() + } + + const handleNew = () => { + window.location.href = '/projects/create' + } + + const handleImport = (file) => { projectsActions.uploadProject('/projects/import/', file) } + + const renderTitle = (title, row) => { + const pathArray = getTitlePath(projects, title, row).split(' / ') + const lastChild = pathArray.pop() + + const catalog = catalogs.find(c => c.id === row.catalog) + + return ( + <div> + <a href={`/projects/${row.id}`}> + {pathArray.map((path, index) => ( + <span key={index}>{path} / </span> + ))} + <b>{lastChild}</b> + </a> + { + catalog && ( + <div className='text-muted' dangerouslySetInnerHTML={{ __html: catalog.title }} ></div> + ) + } + </div> + ) + } + + const loadMore = () => { + const page = get(config, 'params.page') ?? 1 + configActions.updateConfig('params.page', (parseInt(page) + 1).toString()) + projectsActions.fetchProjects(false) + } + + const renderLoadButtons = () => { + return ( + <div className="icon-container ml-auto"> + {projects.length > 0 && showTopButton && + <button className="elliptic-button" onClick={scrollToTop} title={gettext('Scroll to top')}> + <i className="fa fa-arrow-up" aria-hidden="true"></i> + </button> + } + {hasNext && + <button onClick={loadMore} className="elliptic-button"> + {gettext('Load more')} + </button> + } + </div> + ) + } + + /* order of elements in 'visibleColumns' corresponds to order of columns in table */ + let visibleColumns = ['title', 'progress', 'last_changed', 'actions'] + let columnWidths + + if (myProjects) { + visibleColumns.splice(2, 0, 'role') + columnWidths = ['35%', '20%', '20%', '20%', '5%'] + } else { + visibleColumns.splice(2, 0, 'created') + visibleColumns.splice(2, 0, 'owner') + columnWidths = ['35%', '10%', '20%', '20%', '20%', '5%'] + } + + const cellFormatters = { + title: (content, row) => renderTitle(content, row), + role: (_content, row) => { + const { rolesString } = getUserRoles(row, currentUserId) + return rolesString + }, + owner: (_content, row) => row.owners.map(owner => `${owner.first_name} ${owner.last_name}`).join('; '), + progress: (_content, row) => getProgressString(row), + created: content => useFormattedDateTime(content, language), + last_changed: content => useFormattedDateTime(content, language), + actions: (_content, row) => { + const rowUrl = `/projects/${row.id}` + const path = `?next=${window.location.pathname}` + const { isProjectManager, isProjectOwner } = getUserRoles(row, currentUserId, ['managers', 'owners']) + return ( + <div className="icon-container"> + {(isProjectManager || isProjectOwner || isManager) && + <Link + href={`${rowUrl}/update`} + className="element-link fa fa-pencil" + title={row.title} + onClick={() => window.location.href = `${rowUrl}/update/${path}`} + /> + } + {(isProjectOwner || isManager) && + <Link + href={`${rowUrl}/delete`} + className="element-link fa fa-trash" + title={row.title} + onClick={() => window.location.href = `${rowUrl}/delete/${path}`} + /> + } + </div> + ) + } + } + + return ( + <div className="projects"> + <div className="projects-header-container"> + <h1>{headline}</h1> + <div className="projects-header-buttons"> + { + !isEmpty(invites) && myProjects && ( + <button className="btn btn-link" onClick={openInvitations}> + <span className="badge badge-primary badge-invitations"> + {invites.length} + </span> + {gettext('Pending invitations')} + </button> + ) + } + { + isManager && ( + <button className="btn btn-link" onClick={handleView}> + {viewLinkText} + </button> + ) + } + <button id="import-project" className="btn btn-link" onClick={openImport}> + <i className="fa fa-download" aria-hidden="true"></i> {gettext('Import project')} + </button> + <button id="create-project" className="btn btn-link" onClick={handleNew}> + <i className="fa fa-plus" aria-hidden="true"></i> {gettext('New project')} + </button> + </div> + </div> + <div className="projects-form"> + <div className="text-muted mb-10"> + {displayMessage} + </div> + <div className="search-container"> + <SearchField + value={searchString} + onChange={updateSearchString} + onSearch={() => projectsActions.fetchProjects()} + placeholder={gettext('Search projects')} + className="search-field" + /> + </div> + <ProjectFilters + catalogs={catalogs ?? []} + config={config} + configActions={configActions} + isManager={isManager} + projectsActions={projectsActions} + /> + </div> + <Table + cellFormatters={cellFormatters} + columnWidths={columnWidths} + config={config} + configActions={configActions} + data={projects} + headerFormatters={HEADER_FORMATTERS} + projectsActions={projectsActions} + sortableColumns={SORTABLE_COLUMNS} + visibleColumns={visibleColumns} + /> + {renderLoadButtons()} + <Modal {...invitationsModalProps}> + <PendingInvitations invitations={invites} /> + </Modal> + <Modal {...importModalProps}> + <ProjectImport + allowedTypes={allowedTypes} + handleImport={handleImport} + importUrls={importUrls ?? []} /> + </Modal> + </div> + ) +} + +Projects.propTypes = { + config: PropTypes.object.isRequired, + configActions: PropTypes.object.isRequired, + currentUserObject: PropTypes.object.isRequired, + projectsActions: PropTypes.object.isRequired, + projectsObject: PropTypes.object.isRequired, +} + +export default Projects diff --git a/rdmo/projects/assets/js/containers/Main.js b/rdmo/projects/assets/js/containers/Main.js new file mode 100644 index 0000000000..234afceeb7 --- /dev/null +++ b/rdmo/projects/assets/js/containers/Main.js @@ -0,0 +1,52 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import * as configActions from '../actions/configActions' +import * as projectsActions from '../actions/projectsActions' +import * as userActions from '../actions/userActions' +import Projects from '../components/main/Projects' + +const Main = ({ config, configActions, projectsActions, projects, userActions, currentUser }) => { + if (projects.ready) { + return ( + <Projects + config={config} + configActions={configActions} + currentUserObject={currentUser} + projectsActions={projectsActions} + projectsObject={projects} + userActions={userActions} + /> + ) + } + + return null +} + +Main.propTypes = { + config: PropTypes.object.isRequired, + configActions: PropTypes.object.isRequired, + currentUser: PropTypes.object.isRequired, + projectsActions: PropTypes.object.isRequired, + projects: PropTypes.object.isRequired, + userActions: PropTypes.object.isRequired +} + +function mapStateToProps(state) { + return { + config: state.config, + currentUser: state.currentUser, + projects: state.projects, + } +} + +function mapDispatchToProps(dispatch) { + return { + configActions: bindActionCreators(configActions, dispatch), + projectsActions: bindActionCreators(projectsActions, dispatch), + userActions: bindActionCreators(userActions, dispatch), + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Main) diff --git a/rdmo/projects/assets/js/hooks/useDatePicker.js b/rdmo/projects/assets/js/hooks/useDatePicker.js new file mode 100644 index 0000000000..68c946a172 --- /dev/null +++ b/rdmo/projects/assets/js/hooks/useDatePicker.js @@ -0,0 +1,48 @@ +import { useState, useMemo, useCallback } from 'react' +import { de } from 'date-fns/locale' +import { camelCase } from 'lodash' +import { language } from 'rdmo/core/assets/js/utils' + +const useDatePicker = () => { + const [dateRange, setDateRange] = useState({ + createdStart: null, + createdEnd: null, + lastChangedStart: null, + lastChangedEnd: null + }) + + const dateFormat = useMemo(() => { + return language === 'de' ? 'dd.MM.yyyy' : 'MM/dd/yyyy' + }, []) + + const getLocale = useCallback((lang) => { + switch (lang) { + case 'de': + return de + default: + return undefined // defaults to English + } + }, []) + + const setStartDate = useCallback((type, date) => { + const camelCaseType = camelCase(type) + const startKey = `${camelCaseType}Start` + setDateRange(prev => ({ ...prev, [startKey]: date })) + }, []) + + const setEndDate = useCallback((type, date) => { + const camelCaseType = camelCase(type) + const endKey = `${camelCaseType}End` + setDateRange(prev => ({ ...prev, [endKey]: date })) + }, []) + + return { + dateRange, + dateFormat, + getLocale, + setStartDate, + setEndDate + } +} + +export default useDatePicker diff --git a/rdmo/projects/assets/js/projects.js b/rdmo/projects/assets/js/projects.js new file mode 100644 index 0000000000..414a8c915f --- /dev/null +++ b/rdmo/projects/assets/js/projects.js @@ -0,0 +1,20 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { Provider } from 'react-redux' + +import configureStore from './store/configureStore' + +import { DndProvider } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' + +import Main from './containers/Main' + +const store = configureStore() + +createRoot(document.getElementById('main')).render( + <DndProvider backend={HTML5Backend}> + <Provider store={store}> + <Main /> + </Provider> + </DndProvider> +) diff --git a/rdmo/projects/assets/js/reducers/configReducer.js b/rdmo/projects/assets/js/reducers/configReducer.js new file mode 100644 index 0000000000..7ee35f2d5a --- /dev/null +++ b/rdmo/projects/assets/js/reducers/configReducer.js @@ -0,0 +1,26 @@ +import { set, unset } from 'lodash' +import { DELETE_CONFIG, UPDATE_CONFIG } from '../actions/actionTypes' + +const initialState = { + myProjects: true, + params: {}, + showFilters: false, +} + +export default function configReducer(state = initialState, action) { + let newState + switch(action.type) { + case UPDATE_CONFIG: + newState = {...state} + set(newState, action.path, action.value) + localStorage.setItem(`rdmo.projects.config.${action.path}`, action.value.toString()) + return newState + case DELETE_CONFIG: + newState = {...state} + unset(newState, action.path) + localStorage.removeItem(`rdmo.projects.config.${action.path}`) + return newState + default: + return state + } +} diff --git a/rdmo/projects/assets/js/reducers/projectsReducer.js b/rdmo/projects/assets/js/reducers/projectsReducer.js new file mode 100644 index 0000000000..c724ade2c6 --- /dev/null +++ b/rdmo/projects/assets/js/reducers/projectsReducer.js @@ -0,0 +1,55 @@ +import { FETCH_PROJECTS_ERROR, FETCH_PROJECTS_INIT, FETCH_PROJECTS_SUCCESS, + FETCH_INVITATIONS_ERROR, FETCH_INVITATIONS_INIT, FETCH_INVITATIONS_SUCCESS, + FETCH_CATALOGS_ERROR, FETCH_CATALOGS_INIT, FETCH_CATALOGS_SUCCESS, + FETCH_FILETYPES_ERROR, FETCH_FILETYPES_INIT, FETCH_FILETYPES_SUCCESS, + FETCH_IMPORT_URLS_ERROR, FETCH_IMPORT_URLS_INIT, FETCH_IMPORT_URLS_SUCCESS, + } from '../actions/actionTypes' + +const initialState = { + ready: false, + projects: [] +} + +export default function projectsReducer(state = initialState, action) { + switch(action.type) { + case FETCH_PROJECTS_INIT: + return {...state, ...action.projects} + case FETCH_PROJECTS_SUCCESS: { + return { + ...state, + projects: action.shouldConcatenate ? [...state.projects, ...action.projects.results] : action.projects.results, + ready: true, + projectsCount: action.projects.count, + hasNext: action.projects.next !== null + } + } + case FETCH_PROJECTS_ERROR: + return {...state, errors: action.error.errors} + case FETCH_INVITATIONS_INIT: + return {...state, ...action.invites} + case FETCH_INVITATIONS_SUCCESS: + return {...state, ...action.invites} + case FETCH_INVITATIONS_ERROR: + return {...state, errors: action.error.errors} + case FETCH_CATALOGS_INIT: + return {...state, ...action.catalogs} + case FETCH_CATALOGS_SUCCESS: + return {...state, ...action.catalogs} + case FETCH_CATALOGS_ERROR: + return {...state, errors: action.error.errors} + case FETCH_FILETYPES_INIT: + return {...state, ...action.allowedTypes} + case FETCH_FILETYPES_SUCCESS: + return {...state, ...action.allowedTypes} + case FETCH_FILETYPES_ERROR: + return {...state, errors: action.error.errors} + case FETCH_IMPORT_URLS_INIT: + return {...state, ...action.importUrls} + case FETCH_IMPORT_URLS_SUCCESS: + return {...state, ...action.importUrls} + case FETCH_IMPORT_URLS_ERROR: + return {...state, errors: action.error.errors} + default: + return state + } +} diff --git a/rdmo/projects/assets/js/reducers/rootReducer.js b/rdmo/projects/assets/js/reducers/rootReducer.js new file mode 100644 index 0000000000..e6eb04adc0 --- /dev/null +++ b/rdmo/projects/assets/js/reducers/rootReducer.js @@ -0,0 +1,12 @@ +import { combineReducers } from 'redux' +import configReducer from './configReducer' +import projectsReducer from './projectsReducer' +import userReducer from './userReducer' + +const rootReducer = combineReducers({ + config: configReducer, + currentUser: userReducer, + projects: projectsReducer, +}) + +export default rootReducer diff --git a/rdmo/projects/assets/js/reducers/userReducer.js b/rdmo/projects/assets/js/reducers/userReducer.js new file mode 100644 index 0000000000..5e759279f0 --- /dev/null +++ b/rdmo/projects/assets/js/reducers/userReducer.js @@ -0,0 +1,19 @@ +import { FETCH_CURRENT_USER_ERROR, FETCH_CURRENT_USER_INIT, FETCH_CURRENT_USER_SUCCESS } + from '../actions/actionTypes' + +const initialState = { + currentUser: {}, +} + +export default function userReducer(state = initialState, action) { + switch(action.type) { + case FETCH_CURRENT_USER_INIT: + return {...state, ...action.currentUser} + case FETCH_CURRENT_USER_SUCCESS: + return {...state, ...action.currentUser} + case FETCH_CURRENT_USER_ERROR: + return {...state, errors: action.error.errors} + default: + return state + } +} diff --git a/rdmo/projects/assets/js/store/configureStore.js b/rdmo/projects/assets/js/store/configureStore.js new file mode 100644 index 0000000000..92b5775e83 --- /dev/null +++ b/rdmo/projects/assets/js/store/configureStore.js @@ -0,0 +1,75 @@ +import { createStore, applyMiddleware } from 'redux' +import thunk from 'redux-thunk' +import Cookies from 'js-cookie' +import isEmpty from 'lodash/isEmpty' +import rootReducer from '../reducers/rootReducer' +import * as userActions from '../actions/userActions' +import * as projectsActions from '../actions/projectsActions' +import * as configActions from '../actions/configActions' +import userIsManager from '../utils/userIsManager' + +export default function configureStore() { + const middlewares = [thunk] + + const currentStoreId = Cookies.get('storeid') + const localStoreId = localStorage.getItem('rdmo.storeid') + + if (isEmpty(localStoreId) || localStoreId !== currentStoreId) { + localStorage.clear() + localStorage.setItem('rdmo.storeid', currentStoreId) + } + + if (process.env.NODE_ENV === 'development') { + const { logger } = require('redux-logger') + middlewares.push(logger) + } + + const store = createStore( + rootReducer, + applyMiddleware(...middlewares) + ) + + // load: restore the config from the local storage + const updateConfigFromLocalStorage = () => { + const ls = {...localStorage} + + Object.entries(ls).forEach(([lsPath, lsValue]) => { + if (lsPath.startsWith('rdmo.projects.config.')) { + const path = lsPath.replace('rdmo.projects.config.', '') + let value + switch(lsValue) { + case 'true': + value = true + break + case 'false': + value = false + break + default: + value = lsValue + } + store.dispatch(configActions.updateConfig(path, value)) + } + }) + } + + window.addEventListener('load', () => { + updateConfigFromLocalStorage() + + Promise.all([ + store.dispatch(userActions.fetchCurrentUser()), + store.dispatch(projectsActions.fetchCatalogs()) + ]).then(() => { + const currentUser = store.getState().currentUser.currentUser + const isManager = userIsManager(currentUser) + if (isManager && store.getState().config.myProjects) { + store.dispatch(configActions.updateConfig('params.user', currentUser.id)) + } + store.dispatch(projectsActions.fetchProjects()) + store.dispatch(projectsActions.fetchInvitations(currentUser.id)) + store.dispatch(projectsActions.fetchAllowedFileTypes()) + store.dispatch(projectsActions.fetchImportUrls()) + }) + }) + + return store +} diff --git a/rdmo/projects/assets/js/utils/constants.js b/rdmo/projects/assets/js/utils/constants.js new file mode 100644 index 0000000000..19c774d8b2 --- /dev/null +++ b/rdmo/projects/assets/js/utils/constants.js @@ -0,0 +1,18 @@ +// projects table +export const SORTABLE_COLUMNS = ['created', 'owner', 'progress', 'role', 'title', 'last_changed'] +export const HEADER_FORMATTERS = { + title: {render: () => gettext('Name')}, + role: {render: () => gettext('Role')}, + owner: {render: () => gettext('Owner')} , + progress: {render: () => gettext('Progress')}, + created: {render: () => gettext('Created')}, + last_changed: {render: () => gettext('Last changed')}, + actions: {render: () => null}, +} +// project roles +export const ROLE_LABELS = { + author: gettext('Author'), + guest: gettext('Guest'), + manager: gettext('Manager'), + owner: gettext('Owner') +} diff --git a/rdmo/projects/assets/js/utils/getProjectTitlePath.js b/rdmo/projects/assets/js/utils/getProjectTitlePath.js new file mode 100644 index 0000000000..e3c17de16a --- /dev/null +++ b/rdmo/projects/assets/js/utils/getProjectTitlePath.js @@ -0,0 +1,24 @@ +import { isNil } from 'lodash' + +export const getParentPath = (projects, parentId, pathArray = []) => { + const parent = projects.find((project) => project.id === parentId) + if (parent) { + const { title: parentTitle, parent: grandParentId } = parent + pathArray.unshift(parentTitle) + if (!isNil(grandParentId) && typeof grandParentId === 'number') { + return getParentPath(projects, grandParentId, pathArray) + } + } + return pathArray +} + +export const getTitlePath = (projects, title, row) => { + let parentPath = '' + if (row.parent) { + const path = getParentPath(projects, row.parent) + parentPath = path.join(' / ') + } + + const pathArray = parentPath ? [parentPath, title] : [title] + return pathArray.join(' / ') +} diff --git a/rdmo/projects/assets/js/utils/getUserRoles.js b/rdmo/projects/assets/js/utils/getUserRoles.js new file mode 100644 index 0000000000..f7a1cfe6be --- /dev/null +++ b/rdmo/projects/assets/js/utils/getUserRoles.js @@ -0,0 +1,37 @@ +import { ROLE_LABELS } from './constants' + +export const getUserRoles = (row, currentUserId, arraysToSearch) => { + if (!arraysToSearch || !arraysToSearch.length) { + arraysToSearch = ['authors', 'guests', 'managers', 'owners'] + } + + const roleDefinitions = { + authors: { roleLabel: ROLE_LABELS.author, roleBoolean: 'isProjectAuthor' }, + guests: { roleLabel: ROLE_LABELS.guest, roleBoolean: 'isProjectGuest' }, + managers: { roleLabel: ROLE_LABELS.manager, roleBoolean: 'isProjectManager' }, + owners: { roleLabel: ROLE_LABELS.owner, roleBoolean: 'isProjectOwner' } + } + + let rolesFound = [] + let roleBooleans = { + isProjectAuthor: false, + isProjectGuest: false, + isProjectManager: false, + isProjectOwner: false + } + + arraysToSearch.forEach(arrayName => { + if (row[arrayName].some(item => item.id === currentUserId)) { + const { roleLabel, roleBoolean } = roleDefinitions[arrayName] + rolesFound.push(roleLabel) + roleBooleans[roleBoolean] = true + } + }) + + return { + rolesString: rolesFound.length > 0 ? rolesFound.join(', ') : null, + ...roleBooleans + } +} + +export default getUserRoles diff --git a/rdmo/projects/assets/js/utils/index.js b/rdmo/projects/assets/js/utils/index.js new file mode 100644 index 0000000000..4927271517 --- /dev/null +++ b/rdmo/projects/assets/js/utils/index.js @@ -0,0 +1,5 @@ +export * from './constants' +export * from './getProjectTitlePath' +export { default as getUserRoles } from './getUserRoles' +export { default as userIsManager } from './userIsManager' +export { default as TRANSLATIONS } from './translations' diff --git a/rdmo/projects/assets/js/utils/translations.js b/rdmo/projects/assets/js/utils/translations.js new file mode 100644 index 0000000000..ff8da5af7d --- /dev/null +++ b/rdmo/projects/assets/js/utils/translations.js @@ -0,0 +1,22 @@ +const TRANSLATIONS = { + accept: gettext('Accept'), + allProjects: gettext('All projects'), + decline: gettext('Decline'), + filter: gettext('Filter'), + hideFilters: gettext('Hide filters'), + importProject: gettext('Import project'), + importFromFile: gettext('Import from file'), + loadAll: gettext('Load all'), + loadMore: gettext('Load more'), + myProjects: gettext('My projects'), + newProject: gettext('New project'), + pendingInvitations: gettext('Pending invitations'), + resetAllFilters: gettext('Reset all filters'), + search: gettext('Search'), + searchProjects: gettext('Search projects'), + showFilters: gettext('Show filters'), + viewAllProjects: gettext('View all projects'), + viewMyProjects: gettext('View my projects'), +} + +export default TRANSLATIONS diff --git a/rdmo/projects/assets/js/utils/userIsManager.js b/rdmo/projects/assets/js/utils/userIsManager.js new file mode 100644 index 0000000000..435d866a33 --- /dev/null +++ b/rdmo/projects/assets/js/utils/userIsManager.js @@ -0,0 +1,11 @@ +import siteId from 'rdmo/core/assets/js/utils/siteId' + +const userIsManager = (currentUser) => { + if (currentUser.is_superuser || + (currentUser.role && currentUser.role.manager && currentUser.role.manager.some(manager => manager.id === siteId))) { + return true + } + return false +} + +export default userIsManager diff --git a/rdmo/projects/assets/scss/projects.scss b/rdmo/projects/assets/scss/projects.scss new file mode 100644 index 0000000000..dbe1811707 --- /dev/null +++ b/rdmo/projects/assets/scss/projects.scss @@ -0,0 +1,376 @@ +$icon-font-path: "bootstrap-sass/assets/fonts/bootstrap/"; +@import '~bootstrap-sass'; +@import '~font-awesome/css/font-awesome.css'; +@import 'rdmo/core/assets/scss/variables'; +@import 'react-datepicker/dist/react-datepicker.css'; + +a.disabled { + cursor: not-allowed; +} + +.flip { + transform: rotate(180deg) scaleX(-1); +} + +.w-100 { + width: 100%; +} +.w-25 { + width: 25%; +} +.mt-0 { + margin-top: 0; +} +.mt-5 { + margin-top: 5px; +} +.mt-10 { + margin-top: 10px; +} +.mt-20 { + margin-top: 20px; +} +.mr-0 { + margin-right: 0; +} +.mr-5 { + margin-right: 5px; +} +.mr-10 { + margin-right: 10px; +} +.mr-20 { + margin-right: 20px; +} +.mb-0 { + margin-bottom: 0; +} +.mb-5 { + margin-bottom: 5px; +} +.mb-10 { + margin-bottom: 10px; +} +.mb-20 { + margin-bottom: 20px; +} +.ml-0 { + margin-left: 0; +} +.ml-5 { + margin-left: 5px; +} +.ml-10 { + margin-left: 10px; +} +.ml-20 { + margin-left: 20px; +} + +.pt-0 { + padding-top: 0; +} +.pt-10 { + padding-top: 10px; +} +.pt-20 { + padding-top: 20px; +} +.pr-0 { + padding-right: 0; +} +.pr-10 { + padding-right: 10px; +} +.pr-20 { + padding-right: 20px; +} +.pb-0 { + padding-bottom: 0; +} +.pb-10 { + padding-bottom: 10px; +} +.pb-20 { + padding-bottom: 20px; +} +.pl-0 { + padding-left: 0; +} +.pl-10 { + padding-left: 10px; +} +.pl-20 { + padding-left: 20px; +} +.align-right { + text-align: right; +} +.align-left { + text-align: left; +} + +.form-group { + .react-select__control { + min-height: 34px; + } + .react-select__value-container { + padding: 0 14px; + } + .react-select__input-container { + margin: 0; + padding: 0; + } + .react-select__indicator { + padding: 6px; + } + .react-select__option { + padding: 0 14px; + } +} + +.list-group-item { + .indent { + padding-left: 20px; + } +} + +.codemirror.form-control { + height: auto; + padding: 0; + + .cm-editor { + border-radius: 4px; + } + .cm-gutter { + background-color: white; + } + .cm-focused { + // copy the .form-control:focus from bootstrap + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + } +} + +.select-item-options, +.multi-select-item-options, +.ordered-multi-select-item-options { + float: right; + display: flex; + gap: 4px; + padding: 10px 0; + + .disabled { + color: #ccc !important; + cursor: not-allowed !important; + } +} + +.ordered-multi-select-item { + margin-bottom: 10px; + + .ordered-multi-select-item-select { + height: 34px; + } +} + +.projects-header-container { + @media (min-width: $screen-sm-max) { + display: flex; + justify-content: space-between; + align-items: end; + + margin-bottom: 10px; + } + + .projects-header-buttons { + @media (min-width: $screen-sm-max) { + display: flex; + gap: 20px; + } + + .btn.btn-link { + padding: 0; + padding-bottom: 15px; + + @media (max-width: $screen-sm-max) { + display: block; + padding-bottom: 10px; + } + } + } +} + +.icon-container { + display: flex; + gap: 5px; + margin-bottom: 10px; +} + +.projects { + min-height: 600px; +} + +.projects-form { + margin-bottom: 40px; +} + +.panel-filters { + border-color: #ccc; + + .react-datepicker-wrapper { + width: 100%; + } + + @media (max-width: $screen-md-max) { + .control-label { + margin-bottom: 10px; + } + .react-select, + .react-datepicker-wrapper { + margin-bottom: 10px; + } + + // remove the margin from the last datepicker + .panel-body > .row > div:last-child .row > div:last-child .react-datepicker-wrapper { + margin-bottom: 0; + } + } +} + +.projects-search { + position: relative; + + .projects-search-reset { + display: block; + position: absolute; + top: 7px; + right: 7px; + z-index: 1020; + + opacity: 0.8; + line-height: 20px; + font-size: 14px; + + margin-left: 2px; + } +} + +.elliptic-button { + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; + display: inline-block; +} + +.elliptic-button:hover, +.elliptic-button:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.row-container { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + color: #666; + padding-left: 5px; +} + +.row-container:not(:last-child):after { /* Add line between each container element */ + content: ''; + width: 100%; + height: 1px; + background-color: #ccc; + margin-top: 10px; + margin-bottom: 10px; +} + +.search-container { + width: 100%; + + .search-field { + width: 100%; + } +} + +.projects-datepicker .react-datepicker { + font-size: 1em; +} +.projects-datepicker .react-datepicker__header { + padding-top: 0.8em; +} +.projects-datepicker .react-datepicker__month { + margin: 0.4em 1em; +} +.projects-datepicker .react-datepicker__day-name, .projects-datepicker .react-datepicker__day { + width: 1.9em; + line-height: 1.9em; + margin: 0.166em; +} +.projects-datepicker .react-datepicker__current-month { + font-size: 1em; +} +.projects-datepicker .react-datepicker__navigation { + top: 1em; + line-height: 1.7em; + border: 0.45em solid transparent; +} +.projects-datepicker .react-datepicker__navigation--previous { + left: 1em; +} +.projects-datepicker .react-datepicker__navigation--next { + right: 1em; +} +.projects-datepicker .react-datepicker__close-icon::after { + background-color: #fff; + color: black; + font-size: 1.5em; +} + +.thead-dark th { + cursor: pointer; +} + +.badge-invitations { + background-color: $link-color; + color: #fff; + margin-right: 5px; +} + +.btn.btn-link { + &:focus { + outline:0; + } +} + +.dropzone-container { + width: 100%; + padding: 10px; +} + +.dropzone { + border: 2px dashed #cccccc; + border-radius: 5px; + padding: 20px; + text-align: center; + transition: border .24s ease-in-out; +} + +.dropzone:focus { + border-color: #2196f3; +} + +/* Add additional styling for when files are being dragged over the dropzone */ +.dropzone.drag-active { + border-color: #2196f3; +} + +/* Add styles for when the dropzone is disabled */ +.dropzone.disabled { + opacity: 0.6; +} diff --git a/rdmo/projects/exports.py b/rdmo/projects/exports.py index 6be2125f7c..141d307050 100644 --- a/rdmo/projects/exports.py +++ b/rdmo/projects/exports.py @@ -157,5 +157,5 @@ def render(self): serializer = ExportSerializer(self.project) xmldata = XMLRenderer().render(serializer.data) response = HttpResponse(prettify_xml(xmldata), content_type="application/xml") - response['Content-Disposition'] = 'filename="%s.xml"' % self.project.title + response['Content-Disposition'] = f'filename="{self.project.title}.xml"' return response diff --git a/rdmo/projects/filters.py b/rdmo/projects/filters.py index 064f712363..211ac992ca 100644 --- a/rdmo/projects/filters.py +++ b/rdmo/projects/filters.py @@ -1,8 +1,13 @@ -from rest_framework.filters import BaseFilterBackend +from django.db.models import F, OuterRef, Q, Subquery +from django.db.models.functions import Concat +from django.utils.dateparse import parse_datetime +from django.utils.timezone import is_aware, make_aware + +from rest_framework.filters import BaseFilterBackend, OrderingFilter, SearchFilter from django_filters import CharFilter, FilterSet -from .models import Project +from .models import Membership, Project class ProjectFilter(FilterSet): @@ -13,6 +18,107 @@ class Meta: fields = ('title', 'catalog') +class ProjectSearchFilterBackend(SearchFilter): + + def filter_queryset(self, request, queryset, view): + if view.detail: + return queryset + + search_terms = self.get_search_terms(request) + if search_terms: + for search_term in search_terms: + queryset = queryset.filter( + Q(title__icontains=search_term) | ( + Q(memberships__role='owner') & ( + Q(memberships__user__username__icontains=search_term) | + Q(memberships__user__first_name__icontains=search_term) | + Q(memberships__user__last_name__icontains=search_term) | + Q(memberships__user__email__icontains=search_term) + ) + ) + ) + + return queryset + + +class ProjectDateFilterBackend(BaseFilterBackend): + + def filter_queryset(self, request, queryset, view): + if view.detail: + return queryset + + created_before = self.parse_query_datetime(request, 'created_before') + if created_before: + queryset = queryset.filter(created__lte=created_before) + + created_after = self.parse_query_datetime(request, 'created_after') + if created_after: + queryset = queryset.filter(created__gte=created_after) + + updated_before = self.parse_query_datetime(request, 'updated_before') + if updated_before: + queryset = queryset.filter(updated__lte=updated_before) + + updated_after = self.parse_query_datetime(request, 'updated_after') + if updated_after: + queryset = queryset.filter(updated__gte=updated_after) + + last_changed_before = self.parse_query_datetime(request, 'last_changed_before') + if last_changed_before: + queryset = queryset.filter(last_changed__lte=last_changed_before) + + last_changed_after = self.parse_query_datetime(request, 'last_changed_after') + if last_changed_after: + queryset = queryset.filter(last_changed__gte=last_changed_after) + + return queryset + + def parse_query_datetime(self, request, key): + value = request.GET.get(key) + if value: + datetime = parse_datetime(value) + if datetime: + if is_aware(datetime): + return datetime + else: + return make_aware(datetime) + + +class ProjectOrderingFilter(OrderingFilter): + + def filter_queryset(self, request, queryset, view): + if view.detail: + return queryset + + ordering = self.get_ordering(request, queryset, view) + if ordering: + if 'owner' in ordering or '-owner' in ordering: + # annotate with the first owner, ordered by last_name, first_name + owner_subquery = Subquery( + Membership.objects.filter(project=OuterRef('pk'), role='owner') \ + .annotate(owner=Concat(F('user__last_name'), F('user__first_name'))) \ + .order_by('owner') \ + [:1].values('owner') + ) + queryset = queryset.annotate(owner=owner_subquery) + + elif 'progress' in ordering or '-progress' in ordering: + # annotate with the progress ratio + queryset = queryset.annotate(progress=(F('progress_count') + F('progress_total'))) + + elif 'role' in ordering or '-role' in ordering: + # annotate with the progress ratio + role_subquery = Subquery( + Membership.objects.filter(project=OuterRef('pk'), user=request.user).values('role') + ) + queryset = queryset.annotate(role=role_subquery) + + # order the queryset + queryset = queryset.order_by(*ordering) + + return queryset + + class SnapshotFilterBackend(BaseFilterBackend): def filter_queryset(self, request, queryset, view): diff --git a/rdmo/projects/forms.py b/rdmo/projects/forms.py index c1ff1145a1..4594fca7fa 100644 --- a/rdmo/projects/forms.py +++ b/rdmo/projects/forms.py @@ -20,24 +20,32 @@ class CatalogChoiceField(forms.ModelChoiceField): _unavailable_icon = ' (<span class="fa fa-eye-slash" aria-hidden="true"></span>)' def label_from_instance(self, obj): + rendered_title = markdown2html(obj.title) + rendered_help = markdown2html(obj.help) + if obj.available is False: - return mark_safe('<div class="text-muted">{}{}</br>{}</div>'.format( - obj.title, self._unavailable_icon, markdown2html(obj.help) - )) + return mark_safe(f'<div class="text-muted"><p>{rendered_title}{self._unavailable_icon}</p>' + f'<p>{rendered_help}</p></div>') - return mark_safe(f'<b>{obj.title}</b></br>{markdown2html(obj.help)}') + return mark_safe(f'<p><b>{rendered_title}</b></p><p>{rendered_help}</p>') class TasksMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return mark_safe(f'<b>{obj.title}</b></br>{markdown2html(obj.text)}') + rendered_title = markdown2html(obj.title) + rendered_text = markdown2html(obj.text) + + return mark_safe(f'<b>{rendered_title}</b></br>{rendered_text}') class ViewsMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return mark_safe(f'<b>{obj.title}</b></br>{markdown2html(obj.help)}') + rendered_title = markdown2html(obj.title) + rendered_help = markdown2html(obj.help) + + return mark_safe(f'<b>{rendered_title}</b></br>{rendered_help}') class ProjectForm(forms.ModelForm): diff --git a/rdmo/projects/imports.py b/rdmo/projects/imports.py index b49c7492cd..f3665111fc 100644 --- a/rdmo/projects/imports.py +++ b/rdmo/projects/imports.py @@ -4,6 +4,7 @@ import mimetypes from django import forms +from django.core.exceptions import ValidationError from django.core.files import File from django.shortcuts import redirect, render from django.utils.translation import gettext_lazy as _ @@ -100,11 +101,21 @@ def check(self): def process(self): if self.current_project is None: catalog_uri = get_uri(self.root.find('catalog'), self.ns_map) + + available_catalogs = Catalog.objects.filter_current_site() \ + .filter_group(self.request.user) \ + .filter_availability(self.request.user) \ + .order_by('order') + try: - self.catalog = Catalog.objects.all().get(uri=catalog_uri) + self.catalog = available_catalogs.get(uri=catalog_uri) except Catalog.DoesNotExist: - log.info('Catalog not in db. Created with uri %s', catalog_uri) - self.catalog = Catalog.objects.all().first() + log.info('Catalog with uri %s does not exists. Used first available catalog.', catalog_uri) + self.catalog = available_catalogs.first() + + if self.catalog is None: + log.info('No catalog is available.') + raise ValidationError('No catalog is available.') self.project = Project() self.project.title = self.root.find('title').text or '' diff --git a/rdmo/projects/management/commands/prune_projects.py b/rdmo/projects/management/commands/prune_projects.py index 41cd724ee3..ce106ff414 100644 --- a/rdmo/projects/management/commands/prune_projects.py +++ b/rdmo/projects/management/commands/prune_projects.py @@ -25,16 +25,16 @@ def handle(self, *args, **options): elif options['min_role'] == 'manager': roles.extend(['manager']) elif options['min_role'] != 'owner': - raise CommandError('Role "%s" does not exist' % options['min_role']) + raise CommandError('Role "{}" does not exist'.format(options['min_role'])) memberships = Membership.objects.filter(role__in=roles).values_list('pk') candidates = Project.objects.exclude(memberships__in=list(memberships)).distinct() if candidates.count() == 0: - self.stdout.write(self.style.SUCCESS('No projects without %s' % (roles))) + self.stdout.write(self.style.SUCCESS(f'No projects without {roles}')) return - self.stdout.write('Found projects without %s:' % (roles)) + self.stdout.write(f'Found projects without {roles}:') for proj in candidates: self.stdout.write(f'{proj} (id={proj.id})') if options['remove']: diff --git a/rdmo/projects/migrations/0061_alter_value_value_type.py b/rdmo/projects/migrations/0061_alter_value_value_type.py new file mode 100644 index 0000000000..c797b9131a --- /dev/null +++ b/rdmo/projects/migrations/0061_alter_value_value_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-07-18 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0060_alter_issue_options'), + ] + + operations = [ + migrations.AlterField( + model_name='value', + name='value_type', + field=models.CharField(choices=[('text', 'Text'), ('url', 'URL'), ('integer', 'Integer'), ('float', 'Float'), ('boolean', 'Boolean'), ('date', 'Date'), ('datetime', 'Datetime'), ('email', 'E-mail'), ('phone', 'Phone'), ('option', 'Option'), ('file', 'File')], default='text', help_text='Type of this value.', max_length=8, verbose_name='Value type'), + ), + ] diff --git a/rdmo/projects/mixins.py b/rdmo/projects/mixins.py index 3b8eb3d7af..516386061c 100644 --- a/rdmo/projects/mixins.py +++ b/rdmo/projects/mixins.py @@ -101,6 +101,7 @@ def import_form(self): import_plugin.current_project = current_project import_plugin.file_name = import_file_name import_plugin.source_title = import_source_title + import_plugin.request = self.request if import_plugin.check(): try: diff --git a/rdmo/projects/models/project.py b/rdmo/projects/models/project.py index d13b33a60a..6425f37f48 100644 --- a/rdmo/projects/models/project.py +++ b/rdmo/projects/models/project.py @@ -107,27 +107,35 @@ def member(self): def owners_str(self): return ', '.join(['' if x is None else str(x) for x in self.user.filter(membership__role='owner')]) - @cached_property + @property def owners(self): - return self.user.filter(memberships__role='owner') + return self.get_members('owner') - @cached_property + @property def managers(self): - return self.user.filter(memberships__role='manager') + return self.get_members('manager') - @cached_property + @property def authors(self): - return self.user.filter(memberships__role='author') + return self.get_members('author') - @cached_property + @property def guests(self): - return self.user.filter(memberships__role='guest') + return self.get_members('guest') @property def file_size(self): queryset = self.values.filter(snapshot=None).exclude(models.Q(file='') | models.Q(file=None)) return sum([value.file.size for value in queryset]) + def get_members(self, role): + try: + # membership_list is created by the Prefetch call in the viewset + return [membership.user for membership in self.memberships_list if membership.role == role] + except AttributeError: + # membership_list does not exist + return self.user.filter(memberships__role=role) + @receiver(pre_delete, sender=Project) def reparent_children(sender, instance, **kwargs): diff --git a/rdmo/projects/progress.py b/rdmo/projects/progress.py index 36eca6d841..efe7421369 100644 --- a/rdmo/projects/progress.py +++ b/rdmo/projects/progress.py @@ -4,45 +4,58 @@ from django.db.models import Exists, OuterRef, Q from rdmo.conditions.models import Condition +from rdmo.core.utils import markdown2html from rdmo.questions.models import Page, Question, QuestionSet -def resolve_conditions(project, values): - # get all conditions for this catalog - pages_conditions_subquery = Page.objects.filter_by_catalog(project.catalog) \ +def get_catalog_conditions(catalog): + pages_conditions_subquery = Page.objects.filter_by_catalog(catalog) \ .filter(conditions=OuterRef('pk')) - questionsets_conditions_subquery = QuestionSet.objects.filter_by_catalog(project.catalog) \ + questionsets_conditions_subquery = QuestionSet.objects.filter_by_catalog(catalog) \ .filter(conditions=OuterRef('pk')) - questions_conditions_subquery = Question.objects.filter_by_catalog(project.catalog) \ + questions_conditions_subquery = Question.objects.filter_by_catalog(catalog) \ .filter(conditions=OuterRef('pk')) - catalog_conditions = Condition.objects.annotate(has_page=Exists(pages_conditions_subquery)) \ - .annotate(has_questionset=Exists(questionsets_conditions_subquery)) \ - .annotate(has_question=Exists(questions_conditions_subquery)) \ - .filter(Q(has_page=True) | Q(has_questionset=True) | Q(has_question=True)) \ - .distinct().select_related('source', 'target_option') + return Condition.objects.annotate( + has_page=Exists(pages_conditions_subquery), + has_questionset=Exists(questionsets_conditions_subquery), + has_question=Exists(questions_conditions_subquery) + ).filter( + Q(has_page=True) | Q(has_questionset=True) | Q(has_question=True) + ).distinct().select_related('source', 'target_option') + + +def resolve_conditions(catalog, values, sets): + # resolve all conditions and return for each condition the set_prefix and set_index for which it resolved true + conditions = defaultdict(set) + if sets: + for condition in get_catalog_conditions(catalog): + conditions[condition.id] = { + (set_prefix, set_index) + for set_prefix, set_index in chain.from_iterable(sets.values()) + if condition.resolve(values, set_prefix=set_prefix, set_index=set_index) + } - # evaluate conditions - conditions = set() - for condition in catalog_conditions: - if condition.resolve(values): - conditions.add(condition.id) - - # return all true conditions for this project return conditions +def compute_sets(values): + # compute sets from values (including empty values) + sets = defaultdict(set) + for attribute, set_prefix, set_index in values.distinct_list(): + sets[attribute].add((set_prefix, set_index)) + return sets + + def compute_navigation(section, project, snapshot=None): # get all values for this project and snapshot values = project.values.filter(snapshot=snapshot).select_related('attribute', 'option') - # get true conditions - conditions = resolve_conditions(project, values) - # compute sets from values (including empty values) - sets = defaultdict(lambda: defaultdict(list)) - for attribute, set_prefix, set_index in values.distinct_list(): - sets[attribute][set_prefix].append(set_index) + sets = compute_sets(values) + + # resolve all conditions to get a dict mapping conditions to set_indexes + conditions = resolve_conditions(project.catalog, values, sets) # query distinct, non empty set values values_list = values.exclude_empty().distinct_list() @@ -52,26 +65,44 @@ def compute_navigation(section, project, snapshot=None): navigation_section = { 'id': catalog_section.id, 'uri': catalog_section.uri, - 'title': catalog_section.title, - 'first': catalog_section.elements[0].id if section.elements else None + 'title': markdown2html(catalog_section.short_title or catalog_section.title), + 'first': catalog_section.elements[0].id if catalog_section.elements else None, + 'count': 0, + 'total': 0 } if catalog_section.id == section.id: navigation_section['pages'] = [] - for page in catalog_section.elements: - pages_conditions = {page.id for page in page.conditions.all()} - show = bool(not pages_conditions or pages_conditions.intersection(conditions)) - # count the total number of questions, taking sets and conditions into account - counts = count_questions(page, sets, conditions) + for page in catalog_section.elements: + pages_conditions = {page.id for page in page.conditions.all()} + + # show only pages with resolved conditions, but show all pages without conditions + if pages_conditions: + # check if any valuesets for set_prefix = '' resolved + # for non collection pages restrict further to set_index = 0 + show = any( + (set_prefix == '') and (page.is_collection or set_index == 0) + for page_condition in pages_conditions + for set_prefix, set_index in conditions[page_condition] + ) + else: + show = True + + # count the total number of questions, taking sets and conditions into account + counts = count_questions(page, sets, conditions) - # filter the values_list for the attributes, and compute the total sum of counts - count = len(tuple(filter(lambda value: value[0] in counts.keys(), values_list))) - total = sum(counts.values()) + # filter the values_list for the attributes, and compute the total sum of counts + count = sum(1 for value in values_list if value[0] in counts) + total = sum(counts.values()) + navigation_section['count'] += count + navigation_section['total'] += total + + if 'pages' in navigation_section: navigation_section['pages'].append({ 'id': page.id, 'uri': page.uri, - 'title': page.title, + 'title': markdown2html(page.short_title or page.title), 'show': show, 'count': count, 'total': total @@ -86,23 +117,20 @@ def compute_progress(project, snapshot=None): # get all values for this project and snapshot values = project.values.filter(snapshot=snapshot).select_related('attribute', 'option') - # get true conditions - conditions = resolve_conditions(project, values) - # compute sets from values (including empty values) - sets = defaultdict(lambda: defaultdict(list)) - for attribute, set_prefix, set_index in values.distinct_list(): - sets[attribute][set_prefix].append(set_index) + sets = compute_sets(values) + + # resolve all conditions to get a dict mapping conditions to set_indexes + conditions = resolve_conditions(project.catalog, values, sets) # query distinct, non empty set values values_list = values.exclude_empty().distinct_list() - # count the total number of questions, taking sets and conditions into account counts = count_questions(project.catalog, sets, conditions) # filter the values_list for the attributes, and compute the total sum of counts - count = len(tuple(filter(lambda value: value[0] in counts.keys(), values_list))) + count = sum(1 for value in values_list if value[0] in counts) total = sum(counts.values()) return count, total @@ -111,39 +139,66 @@ def compute_progress(project, snapshot=None): def count_questions(element, sets, conditions): counts = defaultdict(int) - # obtain the maximum number of set-distinct values for the questions in this page or - # question set this number is how often each question is displayed and we will use - # this number to determine how often a question needs to be counted - if isinstance(element, (Page, QuestionSet)) and element.is_collection: - counted_sets = set() + if isinstance(element, (Page, QuestionSet)): + # obtain the maximum number of set-distinct values for the questions in this page or + # question set. this number is how often each question is displayed and we will use + # this number to determine how often a question needs to be counted + element_sets = set() # count the sets for the id attribute of the page or question if element.attribute is not None: - # nested loop over the separate set_index lists in sets[element.attribute.id] - for set_index in chain.from_iterable(sets[element.attribute.id].values()): - counted_sets.add(set_index) + # nested loop over the separate set_prefix, set_index lists in sets[element.attribute.id] + for set_prefix, set_index in sets[element.attribute.id]: + element_sets.add((set_prefix, set_index)) # count the sets for the questions in the page or question for child in element.elements: if isinstance(child, Question): if child.attribute is not None: - # nested loop over the separate set_index lists in sets[element.attribute.id] - for set_index in chain.from_iterable(sets[child.attribute.id].values()): - counted_sets.add(set_index) + # nested loop over the separate set_prefix, set_index lists in sets[element.attribute.id] + for set_prefix, set_index in sets[child.attribute.id]: + element_sets.add((set_prefix, set_index)) - set_count = len(counted_sets) - else: - set_count = 1 + # look for the elements conditions + element_conditions = {condition.id for condition in element.conditions.all()} + + # if this element has conditions: check if those conditions resolve for the found sets + if element_conditions: + # compute the intersection of the conditions of this child with the full set of conditions + element_condition_intersection = { + (set_prefix, set_index) + for condition_id, condition in conditions.items() + for set_prefix, set_index in conditions[condition_id] + if condition_id in element_conditions + } + + resolved_sets = element_sets.intersection(element_condition_intersection) + if resolved_sets: + set_count = len(resolved_sets) if element.is_collection else 1 + else: + # return immediately and do not consider children, this page/questionset is hidden + return counts + else: + set_count = len(element_sets) if element.is_collection else 1 # loop over all children of this element for child in element.elements: - # look for the elements conditions + # look for the child element's conditions if isinstance(child, (Page, QuestionSet, Question)): child_conditions = {condition.id for condition in child.conditions.all()} else: - child_conditions = [] + child_conditions = set() + + # compute the intersection of the conditions of this child with the full set of conditions + child_condition_intersection = { + (set_prefix, set_index) + for condition_id, condition in conditions.items() + for set_prefix, set_index in conditions[condition_id] + if condition_id in child_conditions + } - if not child_conditions or child_conditions.intersection(conditions): + # check if the element either has no condition or its conditions intersect with the full set of conditions + if not child_conditions or child_condition_intersection: if isinstance(child, Question): # for regular questions add the set_count to the counts dict, since the # question should be answered in every set @@ -152,10 +207,17 @@ def count_questions(element, sets, conditions): # use the max function, since the same attribute could appear twice in the tree if child.attribute is not None: if child.is_optional: - child_count = sum(len(set_indexes) for set_indexes in sets[child.attribute.id].values()) + child_count = len(sets[child.attribute.id]) counts[child.attribute.id] = max(counts[child.attribute.id], child_count) else: - counts[child.attribute.id] = max(counts[child.attribute.id], set_count) + if child_condition_intersection: + # update the set_count for the current question (child element) + # count only the sets that have conditions resolved to true + current_count = len(element_sets.intersection(child_condition_intersection)) + else: + current_count = set_count + + counts[child.attribute.id] = max(counts[child.attribute.id], current_count) else: # for everything else, call this function recursively counts.update(count_questions(child, sets, conditions)) diff --git a/rdmo/projects/renderers.py b/rdmo/projects/renderers.py index 7833ff09ae..2a3e543418 100644 --- a/rdmo/projects/renderers.py +++ b/rdmo/projects/renderers.py @@ -13,25 +13,25 @@ def render_document(self, xml, project): self.render_text_element(xml, 'description', {}, project['description']) self.render_text_element(xml, 'catalog', {'dc:uri': project['catalog']}, None) - if 'tasks' in project and project['tasks']: + if project.get('tasks'): xml.startElement('tasks', {}) for task in project['tasks']: self.render_text_element(xml, 'task', {'dc:uri': task}, None) xml.endElement('tasks') - if 'views' in project and project['views']: + if project.get('views'): xml.startElement('views', {}) for view in project['views']: self.render_text_element(xml, 'view', {'dc:uri': view}, None) xml.endElement('views') - if 'snapshots' in project and project['snapshots']: + if project.get('snapshots'): xml.startElement('snapshots', {}) for snapshot in project['snapshots']: self.render_snapshot(xml, snapshot) xml.endElement('snapshots') - if 'values' in project and project['values']: + if project.get('values'): xml.startElement('values', {}) for value in project['values']: self.render_value(xml, value) @@ -46,7 +46,7 @@ def render_snapshot(self, xml, snapshot): self.render_text_element(xml, 'title', {}, snapshot['title']) self.render_text_element(xml, 'description', {}, snapshot['description']) - if 'values' in snapshot and snapshot['values']: + if snapshot.get('values'): xml.startElement('values', {}) for value in snapshot['values']: self.render_value(xml, value) diff --git a/rdmo/projects/serializers/v1/__init__.py b/rdmo/projects/serializers/v1/__init__.py index 86b3039391..8816dc9ca7 100644 --- a/rdmo/projects/serializers/v1/__init__.py +++ b/rdmo/projects/serializers/v1/__init__.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib.auth import get_user_model +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -8,7 +9,7 @@ from rdmo.services.validators import ProviderValidator from ...models import Integration, IntegrationOption, Invite, Issue, IssueResource, Membership, Project, Snapshot, Value -from ...validators import ValueConflictValidator, ValueQuotaValidator +from ...validators import ValueConflictValidator, ValueQuotaValidator, ValueTypeValidator class UserSerializer(serializers.ModelSerializer): @@ -50,6 +51,8 @@ def get_queryset(self): authors = UserSerializer(many=True, read_only=True) guests = UserSerializer(many=True, read_only=True) + last_changed = serializers.DateTimeField(read_only=True) + class Meta: model = Project fields = ( @@ -66,6 +69,7 @@ class Meta: 'guests', 'created', 'updated', + 'last_changed', 'site', 'views', 'progress_total', @@ -263,7 +267,8 @@ class Meta: ) validators = ( ValueConflictValidator(), - ValueQuotaValidator() + ValueQuotaValidator(), + ValueTypeValidator() ) @@ -306,6 +311,19 @@ class Meta: 'timestamp' ) +class UserInviteSerializer(InviteSerializer): + + title = serializers.CharField(source='project.title') + description = serializers.CharField(source='project.description') + + class Meta: + model = Invite + fields = ( + *InviteSerializer.Meta.fields, + 'title', + 'description', + 'token', + ) class IssueResourceSerializer(serializers.ModelSerializer): diff --git a/rdmo/projects/serializers/v1/overview.py b/rdmo/projects/serializers/v1/overview.py index cc5cfdf3e2..67b21bb599 100644 --- a/rdmo/projects/serializers/v1/overview.py +++ b/rdmo/projects/serializers/v1/overview.py @@ -1,10 +1,13 @@ from rest_framework import serializers +from rdmo.core.serializers import MarkdownSerializerMixin from rdmo.projects.models import Project from rdmo.questions.models import Catalog -class CatalogSerializer(serializers.ModelSerializer): +class CatalogSerializer(MarkdownSerializerMixin, serializers.ModelSerializer): + + markdown_fields = ('title', ) class Meta: model = Catalog diff --git a/rdmo/projects/serializers/v1/page.py b/rdmo/projects/serializers/v1/page.py index be2060815c..5918b8a0a1 100644 --- a/rdmo/projects/serializers/v1/page.py +++ b/rdmo/projects/serializers/v1/page.py @@ -4,6 +4,7 @@ from rdmo.conditions.models import Condition from rdmo.core.serializers import ElementModelSerializerMixin, MarkdownSerializerMixin +from rdmo.core.utils import markdown2html from rdmo.options.models import Option, OptionSet from rdmo.questions.models import Page, Question, QuestionSet from rdmo.questions.utils import get_widget_class @@ -53,7 +54,7 @@ class Meta: class QuestionSerializer(ElementModelSerializerMixin, MarkdownSerializerMixin, serializers.ModelSerializer): - markdown_fields = ('help', ) + markdown_fields = ('help', 'text') model = serializers.SerializerMethodField() conditions = ConditionSerializer(default=None, many=True) @@ -102,7 +103,7 @@ def get_widget_class(self, obj): class QuestionSetSerializer(ElementModelSerializerMixin, MarkdownSerializerMixin, serializers.ModelSerializer): - markdown_fields = ('help', ) + markdown_fields = ('title', 'help') model = serializers.SerializerMethodField() elements = serializers.SerializerMethodField() @@ -135,7 +136,7 @@ def get_verbose_name(self, obj): class PageSerializer(MarkdownSerializerMixin, serializers.ModelSerializer): - markdown_fields = ('help', ) + markdown_fields = ('title', 'help') elements = serializers.SerializerMethodField() section = serializers.SerializerMethodField() @@ -170,7 +171,7 @@ def get_section(self, obj): section = self.context['catalog'].get_section_for_page(obj) return { 'id': section.id, - 'title': section.title, + 'title': markdown2html(section.title), 'first': section.elements[0].id if section.elements else None } if section else {} diff --git a/rdmo/projects/static/projects/js/project_questions/services.js b/rdmo/projects/static/projects/js/project_questions/services.js index 1d5fbc84d5..2537843d03 100644 --- a/rdmo/projects/static/projects/js/project_questions/services.js +++ b/rdmo/projects/static/projects/js/project_questions/services.js @@ -71,7 +71,7 @@ angular.module('project_questions') service.init = function(project_id) { service.settings = resources.settings.get(); - service.project = resources.projects.get({id: project_id, detail_action: 'overview'}); + service.project = resources.projects.get({id: project_id, detail_action: 'overview'}) $q.all([ service.settings.$promise, @@ -89,6 +89,9 @@ angular.module('project_questions') service.initView(path); } + // mark the catalog title 'safe' + service.project.catalog.title = $sce.trustAsHtml(service.project.catalog.title); + // enable back/forward button of browser, i.e. location changes $rootScope.$on('$locationChangeSuccess', function (scope, next, current) { var path = $location.path().replace(/\//g, ''); @@ -284,17 +287,35 @@ angular.module('project_questions') id: service.project.id, detail_id: future.page.section.id, detail_action: 'navigation' + }, function(response) { + return service.initNavigation(response) }); return future.navigation.$promise }; + service.initNavigation = function(navigation) { + navigation.forEach(function(section) { + section.title = $sce.trustAsHtml(section.title); + + if (angular.isDefined(section.pages)) { + section.pages.forEach(function(page) { + page.title = $sce.trustAsHtml(page.title); + }) + } + }) + + return navigation; + } + service.initPage = function(page) { // store attributes in a separate array if (page.attribute !== null) future.attributes.push(page.attribute); - // mark the help text of the question set 'save' + // mark the help text of the question set 'safe' page.help = $sce.trustAsHtml(page.help); + page.title = $sce.trustAsHtml(page.title); + page.section.title = $sce.trustAsHtml(page.section.title); // init questions and question sets page.elements.forEach(function(element) { @@ -312,8 +333,9 @@ angular.module('project_questions') // store questionsets in a separate array future.questionsets.push(questionset); - // mark the help text of the question set 'save' + // mark the help text of the question set 'safe' questionset.help = $sce.trustAsHtml(questionset.help); + questionset.title = $sce.trustAsHtml(questionset.title); // init questions and question sets questionset.elements.forEach(function(element) { @@ -334,6 +356,7 @@ angular.module('project_questions') // mark the help text of the question set 'save' question.help = $sce.trustAsHtml(question.help); + question.text = $sce.trustAsHtml(question.text); // this is a question! question.isQuestion = true; @@ -495,8 +518,8 @@ angular.module('project_questions') // loop over the page and all questionsets to check conditions angular.forEach([future.page].concat(future.questionsets), function(questionset) { - angular.forEach(future.valuesets[questionset.id], function(valuesets, set_prefix) { - angular.forEach(valuesets, function(valueset, set_index) { + angular.forEach(future.valuesets[questionset.id], function(valuesets) { + angular.forEach(valuesets, function(valueset) { angular.forEach(questionset.elements, function(element) { if (element.model == 'questions.questionset') { var qs = element; @@ -505,8 +528,8 @@ angular.module('project_questions') id: service.project.id, detail_action: 'resolve', questionset: qs.id, - set_prefix: set_prefix, - set_index: set_index + set_prefix: valueset.set_prefix, + set_index: valueset.set_index }, function(response) { valueset.hidden.questionsets[qs.id] = !response.result; }).$promise); @@ -518,8 +541,8 @@ angular.module('project_questions') id: service.project.id, detail_action: 'resolve', question: q.id, - set_prefix: set_prefix, - set_index: set_index + set_prefix: valueset.set_prefix, + set_index: valueset.set_index }, function(response) { valueset.hidden.questions[q.id] = !response.result; }).$promise); @@ -531,8 +554,8 @@ angular.module('project_questions') id: service.project.id, detail_action: 'resolve', optionset: optionset.id, - set_prefix: set_prefix, - set_index: set_index + set_prefix: valueset.set_prefix, + set_index: valueset.set_index }, function(response) { valueset.hidden.optionsets[optionset.id] = !response.result; }).$promise); @@ -773,9 +796,11 @@ angular.module('project_questions') if (question === null) { // this is the id of a new valueset value.value_type = 'text'; + value.widget_type = 'text'; value.unit = ''; } else { value.value_type = question.value_type; + value.widget_type = question.widget_type; value.unit = question.unit; } @@ -859,10 +884,14 @@ angular.module('project_questions') if (response.status == 500) { service.error = response; } else if (response.status == 400) { - service.error = true; - value.errors = Object.keys(response.data); + service.error = response; + if (angular.isDefined(response.data.text)) { + value.errors = response.data.text + } else { + value.errors = Object.keys(response.data); + } } else if (response.status == 404) { - service.error = true; + service.error = response; value.errors = ['not_found'] } }) @@ -915,8 +944,8 @@ angular.module('project_questions') service.error = null; // reset error before saving if (service.settings.project_questions_autosave) { service.save(false, true).then(function() { - if (service.error !== null) { - // pass, dont jump + if (service.error !== null && service.error.status !== 400) { + // pass, dont jump (but not on a validation error) } else if (angular.isDefined(page)) { service.initView(page.id); } else if (angular.isDefined(section)) { @@ -941,118 +970,127 @@ angular.module('project_questions') return service.storeValues().then(function() { if (service.error !== null) { // pass - } else if (service.page.id == false) { - // pass, the interview is done - } else if (angular.isDefined(proceed) && proceed) { - if (service.settings.project_questions_cycle_sets && service.page.is_collection) { - if (service.set_index === null) { - service.next(); - } else { - var valuesets = service.valuesets[service.page.id][service.set_prefix]; - var index = service.findIndex(valuesets, 'set_index', service.set_index); - - if (index === valuesets.length - 1) { - // this is the last valueset, go to the next page + } else { + service.updateProgress() + + if (service.page.id == false) { + // pass, the interview is done + } else if (angular.isDefined(jump) && jump) { + // after the jump initView is called, so we do not need to do anything here + } else if (angular.isDefined(proceed) && proceed) { + if (service.settings.project_questions_cycle_sets && service.page.is_collection) { + if (service.set_index === null) { service.next(); } else { - // activate the next valueset - service.set_index = valuesets[index + 1].set_index; - $window.scrollTo(0, 0); + var valuesets = service.valuesets[service.page.id][service.set_prefix]; + var index = service.findIndex(valuesets, 'set_index', service.set_index); + + if (index === valuesets.length - 1) { + // this is the last valueset, go to the next page + service.next(); + } else { + // activate the next valueset + service.set_index = valuesets[index + 1].set_index; + $window.scrollTo(0, 0); + } } + } else { + service.next(); } } else { - service.next(); - } - } else { - // update progress - if (service.project.read_only !== true) { - resources.projects.postAction({ - id: service.project.id, - detail_action: 'progress' - }, function(response) { - service.progress = response - }); + service.updateView(); } + } + }); + }; + + service.updateProgress = function() { + if (service.project.read_only !== true) { + resources.projects.postAction({ + id: service.project.id, + detail_action: 'progress' + }, function(response) { + service.progress = response + }); + } + } - // update navigation - if (service.project.read_only !== true) { - resources.projects.query({ - id: service.project.id, - detail_id: future.page.section.id, - detail_action: 'navigation' - }, function(response) { - service.navigation = response + service.updateView = function() { + // check if we need to refresh the site + angular.forEach([service.page].concat(service.questionsets), function(questionset) { + angular.forEach(questionset.elements, function(element) { + if (element.model == 'questions.question') { + var question = element; + angular.forEach(question.optionsets, function(optionset) { + if (optionset.has_refresh) { + return service.initView(service.page.id, false); + } }); } + }); + }); + // update navigation + if (service.project.read_only !== true) { + resources.projects.query({ + id: service.project.id, + detail_id: service.page.section.id, + detail_action: 'navigation' + }, function(response) { + service.navigation = service.initNavigation(response) + }); + } - // check if we need to refresh the site - if (angular.isUndefined(jump) || !jump) { - angular.forEach([service.page].concat(service.questionsets), function(questionset) { - angular.forEach(questionset.elements, function(element) { - if (element.model == 'questions.question') { - var question = element; - angular.forEach(question.optionsets, function(optionset) { - if (optionset.has_refresh) { - return service.initView(service.page.id, false); - } - }); + // re-evaluate conditions + angular.forEach([service.page].concat(service.questionsets), function(questionset) { + angular.forEach(service.valuesets[questionset.id], function(valuesets) { + angular.forEach(valuesets, function(valueset) { + angular.forEach(questionset.elements, function(element) { + if (element.model == 'questions.questionset') { + var qs = element; + if (qs.has_conditions) { + promises.push(resources.projects.get({ + id: service.project.id, + detail_action: 'resolve', + questionset: qs.id, + set_prefix: valueset.set_prefix, + set_index: valueset.set_index + }, function(response) { + valueset.hidden.questionsets[qs.id] = !response.result; + }).$promise); } - }); - }); - } - // re-evaluate conditions - angular.forEach([service.page].concat(service.questionsets), function(questionset) { - angular.forEach(service.valuesets[questionset.id], function(valuesets, set_prefix) { - angular.forEach(valuesets, function(valueset, set_index) { - angular.forEach(questionset.elements, function(element) { - if (element.model == 'questions.questionset') { - var qs = element; - if (qs.has_conditions) { - promises.push(resources.projects.get({ - id: service.project.id, - detail_action: 'resolve', - questionset: qs.id, - set_prefix: set_prefix, - set_index: set_index - }, function(response) { - valueset.hidden.questionsets[qs.id] = !response.result; - }).$promise); - } - } else { - var q = element; - if (q.has_conditions) { - promises.push(resources.projects.get({ - id: service.project.id, - detail_action: 'resolve', - question: q.id, - set_prefix: set_prefix, - set_index: set_index - }, function(response) { - valueset.hidden.questions[q.id] = !response.result; - }).$promise); - } - angular.forEach(q.optionsets, function(optionset) { - if (optionset.has_conditions) { - promises.push(resources.projects.get({ - id: service.project.id, - detail_action: 'resolve', - optionset: optionset.id, - set_prefix: set_prefix, - set_index: set_index - }, function(response) { - valueset.hidden.optionsets[optionset.id] = !response.result; - }).$promise); - } - }); + } else { + var q = element; + if (q.has_conditions) { + promises.push(resources.projects.get({ + id: service.project.id, + detail_action: 'resolve', + question: q.id, + set_prefix: valueset.set_prefix, + set_index: valueset.set_index + }, function(response) { + valueset.hidden.questions[q.id] = !response.result; + }).$promise); + } + angular.forEach(q.optionsets, function(optionset) { + if (optionset.has_conditions) { + promises.push(resources.projects.get({ + id: service.project.id, + detail_action: 'resolve', + optionset: optionset.id, + set_prefix: valueset.set_prefix, + set_index: valueset.set_index + }, function(response) { + valueset.hidden.optionsets[optionset.id] = !response.result; + }).$promise); } }); - }); + } }); }); - } + }); }); - }; + } service.changed = function(value, autosave=false) { value.changed = true; @@ -1205,7 +1243,9 @@ angular.module('project_questions') var last_set_index = valuesets[0].set_index; set_index = last_set_index + 1; } - service.valuesets[questionset.id][set_prefix].push(factories.valuesets(set_prefix, set_index)); + + var valueset = factories.valuesets(set_prefix, set_index); + service.valuesets[questionset.id][set_prefix].push(valueset); // loop over questions to initialize them with at least one value, and init checkboxes and widgets angular.forEach(questionset.elements, function(element) { @@ -1230,6 +1270,10 @@ angular.module('project_questions') service.values[question.attribute][set_prefix][set_index] = service.initCheckbox(question, service.values[question.attribute][set_prefix][set_index]); } + if (question.has_conditions) { + valueset.hidden.questions[question.id] = true + } + angular.forEach(service.values[question.attribute][set_prefix][set_index], function(value) { service.initWidget(question, value); }); @@ -1256,7 +1300,10 @@ angular.module('project_questions') if (angular.isUndefined(service.values[questionset.attribute][set_prefix][set_index][0])) { service.values[questionset.attribute][set_prefix][set_index].push(value); } - service.storeValue(value, null, set_prefix, set_index, 0) + service.storeValue(value, null, set_prefix, set_index, 0).then(function(value) { + service.updateProgress(); + service.updateView(); + }) } // recursively loop over child questionsets and sets @@ -1346,6 +1393,9 @@ angular.module('project_questions') service.removeValueSet(qs, sp, si); }); } + + service.updateProgress(); + service.updateView(); }); // if this is the top level questionset, diff --git a/rdmo/projects/templates/projects/overlays/projects_create_project_de.html b/rdmo/projects/templates/projects/overlays/projects_create_project_de.html index 6d8902fe87..e01dceab88 100644 --- a/rdmo/projects/templates/projects/overlays/projects_create_project_de.html +++ b/rdmo/projects/templates/projects/overlays/projects_create_project_de.html @@ -1,6 +1,6 @@ <div class="overlay" id="create-project-overlay" data-title="Neues Projekt anlegen" - data-placement="left auto"> + data-placement="bottom"> Als ersten Schritt können Sie mit diesem Link ein neues Projekt anlegen. diff --git a/rdmo/projects/templates/projects/overlays/projects_create_project_en.html b/rdmo/projects/templates/projects/overlays/projects_create_project_en.html index 6907ccaa78..611353a84f 100644 --- a/rdmo/projects/templates/projects/overlays/projects_create_project_en.html +++ b/rdmo/projects/templates/projects/overlays/projects_create_project_en.html @@ -1,6 +1,6 @@ <div class="overlay" id="create-project-overlay" data-title="Create new project" - data-placement="left auto"> + data-placement="bottom"> As a first step you can create a new project using this link. diff --git a/rdmo/projects/templates/projects/overlays/projects_create_project_fr.html b/rdmo/projects/templates/projects/overlays/projects_create_project_fr.html index 1c643897e0..ea6bada50a 100644 --- a/rdmo/projects/templates/projects/overlays/projects_create_project_fr.html +++ b/rdmo/projects/templates/projects/overlays/projects_create_project_fr.html @@ -1,6 +1,6 @@ <div class="overlay" id="create-project-overlay" data-title="Créer un nouveau projet " - data-placement="left auto"> + data-placement="bottom"> Dans un premier temps, vous pouvez créer un nouveau projet en utilisant ce lien. diff --git a/rdmo/projects/templates/projects/overlays/projects_create_project_it.html b/rdmo/projects/templates/projects/overlays/projects_create_project_it.html index bef98a5ed4..671269e6fb 100644 --- a/rdmo/projects/templates/projects/overlays/projects_create_project_it.html +++ b/rdmo/projects/templates/projects/overlays/projects_create_project_it.html @@ -1,6 +1,6 @@ <div class="overlay" id="create-project-overlay" data-title="Crea nuovo progetto " - data-placement="left auto"> + data-placement="bottom"> Come primo passo puoi creare un nuovo progetto usando questo link. diff --git a/rdmo/projects/templates/projects/overlays/projects_import_project_de.html b/rdmo/projects/templates/projects/overlays/projects_import_project_de.html index 7eb7906c7c..2b53a274d2 100644 --- a/rdmo/projects/templates/projects/overlays/projects_import_project_de.html +++ b/rdmo/projects/templates/projects/overlays/projects_import_project_de.html @@ -1,10 +1,10 @@ <div class="overlay" id="import-project-overlay" data-title="Vorhandenes Projekt importieren" - data-placement="left auto"> + data-placement="bottom"> Sollten Sie bereits ein Projekt in RDMO angelegt und exportiert haben und möchten es Ihrer Projektliste an dieser Stelle hinzufügen, - so können sie hier ein vorhandenes Projekt importieren. + so können Sie hier ein vorhandenes Projekt importieren. {% include 'overlays/buttons.html'%} </div> diff --git a/rdmo/projects/templates/projects/overlays/projects_import_project_en.html b/rdmo/projects/templates/projects/overlays/projects_import_project_en.html index 2746f78a94..241f02018c 100644 --- a/rdmo/projects/templates/projects/overlays/projects_import_project_en.html +++ b/rdmo/projects/templates/projects/overlays/projects_import_project_en.html @@ -1,6 +1,6 @@ <div class="overlay" id="import-project-overlay" data-title="Import existing project" - data-placement="left auto"> + data-placement="bottom"> If you have already created and exported a project in RDMO and would like to add it to your project list, diff --git a/rdmo/projects/templates/projects/overlays/projects_import_project_fr.html b/rdmo/projects/templates/projects/overlays/projects_import_project_fr.html index 5e7f7cf30a..a4451073f9 100644 --- a/rdmo/projects/templates/projects/overlays/projects_import_project_fr.html +++ b/rdmo/projects/templates/projects/overlays/projects_import_project_fr.html @@ -1,6 +1,6 @@ <div class="overlay" id="import-project-overlay" data-title="Importer un projet existant" - data-placement="left auto"> + data-placement="bottom"> Si vous avez déjà créé et exporté un projet dans RDMO et que vous souhaitez l'ajouter à votre liste de projets, diff --git a/rdmo/projects/templates/projects/overlays/projects_import_project_it.html b/rdmo/projects/templates/projects/overlays/projects_import_project_it.html index 5d85048739..29e4155b40 100644 --- a/rdmo/projects/templates/projects/overlays/projects_import_project_it.html +++ b/rdmo/projects/templates/projects/overlays/projects_import_project_it.html @@ -1,6 +1,6 @@ <div class="overlay" id="import-project-overlay" data-title="Importazione di un progetto esistente" - data-placement="left auto"> + data-placement="bottom"> Se hai già creato ed esportato un progetto in RDMO e vuoi aggiungerlo alla tua lista di progetti diff --git a/rdmo/projects/templates/projects/project_answers_element.html b/rdmo/projects/templates/projects/project_answers_element.html index c1d04ce366..c910686f0e 100644 --- a/rdmo/projects/templates/projects/project_answers_element.html +++ b/rdmo/projects/templates/projects/project_answers_element.html @@ -1,9 +1,10 @@ +{% load core_tags %} {% load view_tags %} {% if element.text %} {# this is a question #} - <p><strong>{{ element.text }}</strong></p> + <p><strong class="question-label">{{ element.text|markdown }}</strong></p> {% get_set_prefixes element.attribute project=project_wrapper as set_prefixes %} {% for set_prefix in set_prefixes %} diff --git a/rdmo/projects/templates/projects/project_answers_tree.html b/rdmo/projects/templates/projects/project_answers_tree.html index be50726517..15cc7572a3 100644 --- a/rdmo/projects/templates/projects/project_answers_tree.html +++ b/rdmo/projects/templates/projects/project_answers_tree.html @@ -1,10 +1,12 @@ +{% load core_tags %} + {% for section in project_wrapper.catalog.sections %} - <h2>{{ section.title }}</h2> + <h2>{{ section.title|markdown }}</h2> {% for page in section.pages %} - <h3>{{ page.title }}</h3> + <h3>{{ page.title|markdown }}</h3> {% for element in page.elements %} diff --git a/rdmo/projects/templates/projects/project_detail_header_catalog.html b/rdmo/projects/templates/projects/project_detail_header_catalog.html index c7d358b19e..2dca50c373 100644 --- a/rdmo/projects/templates/projects/project_detail_header_catalog.html +++ b/rdmo/projects/templates/projects/project_detail_header_catalog.html @@ -9,5 +9,9 @@ </p> {% endif %} -<strong>{{ project.catalog.title }}</strong><br/> -{{ project.catalog.help|markdown }} +<p> + <strong>{{ project.catalog.title|markdown }}</strong> +</p> +<p> + {{ project.catalog.help|markdown }} +</p> diff --git a/rdmo/projects/templates/projects/project_detail_issues.html b/rdmo/projects/templates/projects/project_detail_issues.html index 075ce2665f..0e51a4257f 100644 --- a/rdmo/projects/templates/projects/project_detail_issues.html +++ b/rdmo/projects/templates/projects/project_detail_issues.html @@ -33,7 +33,7 @@ <h2>{% trans 'Tasks' %}</h2> <tr> <td> <a href="{% url 'issue' project.pk issue.pk %}"> - {{ issue.task.title }} + {{ issue.task.title|markdown }} </a> </td> <td>{{ issue.task.text|markdown }}</td> diff --git a/rdmo/projects/templates/projects/project_detail_memberships.html b/rdmo/projects/templates/projects/project_detail_memberships.html index f36a7fcbe1..294d4c451d 100644 --- a/rdmo/projects/templates/projects/project_detail_memberships.html +++ b/rdmo/projects/templates/projects/project_detail_memberships.html @@ -1,5 +1,6 @@ {% load i18n %} {% load rules %} +{% load static %} {% load accounts_tags %} {% has_perm 'projects.add_membership_object' request.user project as can_add_membership %} @@ -32,6 +33,7 @@ <h2>{% trans 'Members' %}</h2> <tr> <td> {% full_name membership.user %} + {% include 'projects/project_detail_memberships_socialaccounts.html' %} </td> <td> <a href="mailto:{{ membership.user.email }}">{{ membership.user.email }}</a> diff --git a/rdmo/projects/templates/projects/project_detail_memberships_socialaccounts.html b/rdmo/projects/templates/projects/project_detail_memberships_socialaccounts.html new file mode 100644 index 0000000000..2ba72de77d --- /dev/null +++ b/rdmo/projects/templates/projects/project_detail_memberships_socialaccounts.html @@ -0,0 +1,9 @@ +{% load static %} + +{% for socialaccount in membership.user.socialaccount_set.all %} +{% if socialaccount.provider == 'orcid' %} +<a href="https://orcid.org/{{ socialaccount.uid }}"> + <img alt="ORCID logo" src="{% static 'accounts/img/orcid_16x16.png' %}" width="16" height="16" /> +</a> +{% endif %} +{% endfor %} diff --git a/rdmo/projects/templates/projects/project_detail_views.html b/rdmo/projects/templates/projects/project_detail_views.html index e3cda0f3fa..ae3ce20fa7 100644 --- a/rdmo/projects/templates/projects/project_detail_views.html +++ b/rdmo/projects/templates/projects/project_detail_views.html @@ -30,7 +30,7 @@ <h2>{% trans 'Views' %}</h2> {% for view in views %} <tr> <td> - <a href="{% url 'project_view' project.pk view.pk %}">{{ view.title }}</a> + <a href="{% url 'project_view' project.pk view.pk %}">{{ view.title|markdown }}</a> </td> <td>{{ view.help|markdown }}</td> <td class="text-right"> diff --git a/rdmo/projects/templates/projects/project_questions.html b/rdmo/projects/templates/projects/project_questions.html index 3f2dc7c7f4..5e8f195599 100644 --- a/rdmo/projects/templates/projects/project_questions.html +++ b/rdmo/projects/templates/projects/project_questions.html @@ -51,9 +51,7 @@ <div class="project-questions-form" ng-show="service.page.id" ng-cloak> {% include 'projects/project_questions_head.html' %} - <h2> - {$ service.page.title $} - </h2> + <h2 ng-bind-html="service.page.title"></h2> <div class="help-text" ng-bind-html="service.page.help"></div> diff --git a/rdmo/projects/templates/projects/project_questions_element.html b/rdmo/projects/templates/projects/project_questions_element.html index cc1de9476e..9bb36eae53 100644 --- a/rdmo/projects/templates/projects/project_questions_element.html +++ b/rdmo/projects/templates/projects/project_questions_element.html @@ -1,6 +1,6 @@ <div class="col-md-12" ng-hide="element.isQuestion" ng-init="questionset = element"> <div ng-hide="valueset.hidden.questionsets[questionset.id]"> - <strong>{$ questionset.title $}</strong> + <strong ng-bind-html="questionset.title"></strong> <div class="help-text" ng-bind-html="questionset.help"></div> diff --git a/rdmo/projects/templates/projects/project_questions_head.html b/rdmo/projects/templates/projects/project_questions_head.html index 2fe2671f73..4e3b2f8792 100644 --- a/rdmo/projects/templates/projects/project_questions_head.html +++ b/rdmo/projects/templates/projects/project_questions_head.html @@ -9,7 +9,7 @@ </li> <li> <a href="" ng-click="service.jump(service.page.section)"> - <span> {$ service.page.section.title $} </span> + <span ng-bind-html="service.page.section.title"></span> </a> </li> </ul> diff --git a/rdmo/projects/templates/projects/project_questions_navigation.html b/rdmo/projects/templates/projects/project_questions_navigation.html index 56de0b0895..50bbb4588a 100644 --- a/rdmo/projects/templates/projects/project_questions_navigation.html +++ b/rdmo/projects/templates/projects/project_questions_navigation.html @@ -5,7 +5,15 @@ <ul class="list-unstyled project-questions-overview"> <li ng-repeat="section in service.navigation"> <a href="" ng-click="service.jump(section)"> - {$ section.title $} + <span ng-bind-html="section.title"></span> + <span ng-show="section.count > 0 && section.count == section.total"> + <i class="fa fa-check" aria-hidden="true"></i> + </span> + <span ng-show="section.count > 0 && section.count != section.total"> + {% blocktrans with section_count='{$ section.count $}' section_total='{$ section.total $}' trimmed %} + ({{ section_count }} of {{ section_total }}) + {% endblocktrans %} + </span> </a> <ul class="list-unstyled" @@ -15,7 +23,7 @@ <a href="" ng-click="service.jump(section, page)" ng-show="page.show"> - <span>{$ page.title $}<span> + <span ng-bind-html="page.title"></span> <span ng-show="page.count > 0 && page.count == page.total"> <i class="fa fa-check" aria-hidden="true"></i> </span> @@ -25,7 +33,7 @@ {% endblocktrans %} </span> </a> - <span class="text-muted" ng-hide="page.show">{$ page.title $}<span> + <span class="text-muted" ng-hide="page.show">{$ page.title $}</span> </li> </ul> </li> diff --git a/rdmo/projects/templates/projects/project_questions_overview.html b/rdmo/projects/templates/projects/project_questions_overview.html index 6058e465a5..c2e00ede84 100644 --- a/rdmo/projects/templates/projects/project_questions_overview.html +++ b/rdmo/projects/templates/projects/project_questions_overview.html @@ -5,7 +5,7 @@ {% trans 'Project' %}: <a href="{% url 'project' project.id %}">{$ service.project.title $}</a> </li> <li> - {% trans 'Catalog' %}: {$ service.project.catalog.title $} + {% trans 'Catalog' %}: <span ng-bind-html="service.project.catalog.title"></span> </li> </ul> diff --git a/rdmo/projects/templates/projects/project_questions_question_label.html b/rdmo/projects/templates/projects/project_questions_question_label.html index 7b98fce542..ee225a6c36 100644 --- a/rdmo/projects/templates/projects/project_questions_question_label.html +++ b/rdmo/projects/templates/projects/project_questions_question_label.html @@ -1,6 +1,5 @@ {% load i18n %} -<div class="form-label" ng-class="{'text-muted': question.is_optional}"> - {$ question.text $} +<div class="form-label question-label" ng-bind-html="question.text" ng-class="{'text-muted': question.is_optional}"> <span ng-show="question.is_optional">{% trans '(optional)' %}</span> </div> diff --git a/rdmo/projects/templates/projects/project_questions_save_error.html b/rdmo/projects/templates/projects/project_questions_save_error.html index e3a5aba785..8af6d4b6d8 100644 --- a/rdmo/projects/templates/projects/project_questions_save_error.html +++ b/rdmo/projects/templates/projects/project_questions_save_error.html @@ -1,7 +1,7 @@ {% load i18n %} {% load core_tags %} - <div class="text-danger" ng-show="service.error.statusText && service.error.status"> + <div class="text-danger" ng-show="service.error.statusText && service.error.status != 400"> <p> {% trans 'An error occurred while saving the answer. Please contact support if this problem persists.' %} </p> diff --git a/rdmo/projects/templates/projects/project_questions_value_errors.html b/rdmo/projects/templates/projects/project_questions_value_errors.html index d8245287ad..b4224ca869 100644 --- a/rdmo/projects/templates/projects/project_questions_value_errors.html +++ b/rdmo/projects/templates/projects/project_questions_value_errors.html @@ -17,5 +17,8 @@ <span ng-show="error == 'quota'"> {% trans 'You reached the file quota for this project.' %} </span> + <span ng-hide="error == 'conflict' || error == 'not_found' || error == 'quota'"> + {$ error $} + </span> </li> </ul> diff --git a/rdmo/projects/templates/projects/projects.html b/rdmo/projects/templates/projects/projects.html index e8b718a720..320e817361 100644 --- a/rdmo/projects/templates/projects/projects.html +++ b/rdmo/projects/templates/projects/projects.html @@ -1,184 +1,42 @@ -{% extends 'core/page.html' %} -{% load i18n %} +{% extends 'core/projects_page.html' %} {% load static %} {% load compress %} -{% load mptt_tags %} +{% load i18n %} {% load core_tags %} -{% load accounts_tags %} -{% load projects_tags %} -{% load rules %} -{% block head %} +{% block vendor %} +{% endblock %} + +{% block css %} {% compress css %} - <link rel="stylesheet" href="{% static 'projects/css/projects.scss' %}" type="text/x-scss" /> <link rel="stylesheet" href="{% static 'overlays/css/overlays.scss' %}" type="text/x-scss" /> {% endcompress %} - {% compress js %} - <script type="text/javascript" src="{% static 'overlays/js/overlays.js' %}" ></script> - {% endcompress %} - <script> - $(document).ready(function() { initOverlays('projects'); }); - </script> -{% endblock %} - -{% block sidebar %} - {% has_perm 'projects.add_project' request.user as can_add_project %} - {% test_rule 'projects.can_view_all_projects' request.user request as can_view_all_projects %} - - {% if can_add_project or can_view_all_projects %} - <h2>{% trans 'Options' %}</h2> - - {% if can_add_project %} - <ul class="list-unstyled"> - <li> - <strong> - <a href="{% url 'project_create' %}" id="create-project"> - {% trans 'Create new project' %} - </a> - </strong> - </li> - </ul> - {% endif %} - - {% if can_view_all_projects %} - <ul class="list-unstyled"> - <li> - <a href="{% url 'site_projects' %}"> - {% blocktrans trimmed with site=request.site %}View all projects on {{ site }}{% endblocktrans %} - </a> - </li> - </ul> - {% endif %} - {% endif %} - - <h2>{% trans 'Filter projects' %}</h2> - <form method="GET" class="projects-search"> - <input type="text" class="form-control" id="search" name="title" placeholder="{% trans 'Search project title' %}" - value="{{request.GET.title }}" /> - <a href="{% url 'projects' %}" class="projects-search-reset"> - <i class="fa fa-times" aria-hidden="true"></i> - </a> - <p class="text-muted"> - <small> - {% blocktrans trimmed %} - {{ number_of_filtered_projects }} of {{ number_of_projects }} projects shown - {% endblocktrans %} - </small> - </p> - </form> - - {% if can_add_project %} - - <h2>{% trans 'Import existing project' %}</h2> - - <ul class="list-unstyled"> - <li id="import-project"> - <p> - <strong>{% trans 'Import from file' %}</strong> - </p> - {% url 'project_create_import' as upload_url %} - {% include 'core/upload_form.html' with upload_url=upload_url %} - </li> - - {% if settings.PROJECT_IMPORTS_LIST %} - <li> - <p> - <strong>{% trans 'Import directly' %}</strong> - </p> - </li> - {% for key, label, class in settings.PROJECT_IMPORTS %} - {% if key in settings.PROJECT_IMPORTS_LIST %} - <li> - <a href="{% url 'project_create_import' key %}" target="_blank"> - {{ label }} - </a> - </li> - {% endif %} - {% endfor %} - {% endif %} - </ul> - - {% endif %} - - {% if invites %} - - <h2>{% trans 'Pending invitations' %}</h2> + <link rel="stylesheet" href="{% static 'projects/css/projects.css' %}" /> + {{ block.super }} +{% endblock %} - <ul class="list-unstyled"> - {% for invite in invites %} - <li> - <a class="pull-right" href="{% url 'project_cancel' invite.token %}"> - <i class="fa fa-times" aria-hidden="true"></i> - </a> - <a href="{% url 'project_join' invite.token %}"> - {{ invite.project.title }} - </a> - </li> - {% endfor %} - </ul> +{% block js %} + <script src="{% static 'jquery/jquery-3.4.1.min.js' %}"></script> + <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script> - <p class="text-muted small"> - {% blocktrans trimmed %} - Click on one of the links to join the projects. - {% endblocktrans %} - </p> - {% endif %} + <script src="{% url 'javascript-catalog' %}"></script> + <script src="{% static 'projects/js/projects.js' %}" defer></script> + <script type="text/javascript" src="{% static 'overlays/js/overlays.js' %}" ></script> + <script> + $(document).ready(function() { + setTimeout(function() {initOverlays('projects')}, 500); + }); + </script> {% endblock %} {% block page %} - <h1>{% trans 'My Projects' %}</h1> - <table class="table projects-table" id="projects-table"> - <thead> - <tr> - <th style="width: 50%;">{% trans 'Name' %}</th> - <th style="width: 10%;">{% trans 'Progress' %}</th> - <th style="width: 10%;">{% trans 'Role' %}</th> - <th style="width: 23%;">{% trans 'Last changed' %}</th> - <th style="width: 7%;"></th> - </tr> - </thead> - <tbody> - {% for project in projects %} - <tr> - <td> - <a class="" href="{% url 'project' project.pk %}"> - {% for ancestor in project.get_ancestors %}{{ ancestor }} / {% endfor %} - <strong>{{ project.title }}</strong> - </a> - </td> - <td> - {% project_progress project %} - </td> - <td> - {{ project.role|projects_role }} - </td> - <td> - {{ project.last_changed }} - </td> - <td class="text-right"> - {% has_perm 'projects.change_project_object' request.user project as can_change_project_object %} - {% has_perm 'projects.delete_project_object' request.user project as can_delete_project_object %} - - {% if can_change_project_object %} - <a href="{% url 'project_update' project.pk %}?next={% url 'projects' %}" class="fa fa-pencil"></a> - {% endif %} - - {% if can_delete_project_object %} - <a href="{% url 'project_delete' project.pk %}?next={% url 'projects' %}" class="fa fa-trash"></a> - {% endif %} - </td> - </tr> - {% endfor %} - </tbody> - </table> + <div id="main" class="projects"></div> <div id="support-info"></div> - {% include 'projects/projects_pagination.html' %} - {% render_lang_template 'projects/overlays/projects_create_project' %} {% render_lang_template 'projects/overlays/projects_projects_table' %} {% render_lang_template 'projects/overlays/projects_import_project' %} diff --git a/rdmo/projects/templates/projects/site_projects.html b/rdmo/projects/templates/projects/site_projects.html deleted file mode 100644 index 73e0909abd..0000000000 --- a/rdmo/projects/templates/projects/site_projects.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends 'core/page.html' %} -{% load i18n %} -{% load static %} -{% load compress %} -{% load core_tags %} -{% load accounts_tags %} -{% load projects_tags %} - -{% block head %} - {% compress css %} - <link rel="stylesheet" href="{% static 'projects/css/projects.scss' %}" type="text/x-scss" /> - {% endcompress %} -{% endblock %} - -{% block sidebar %} - <h2>{% trans 'Filter projects' %}</h2> - - <form method="GET" id="projects-search" class="projects-search"> - <input type="text" class="form-control" id="search" name="title" placeholder="{% trans 'Search project title' %}" - value="{{request.GET.title }}" /> - <a href="{% url 'site_projects' %}" class="projects-search-reset"> - <i class="fa fa-times" aria-hidden="true"></i> - </a> - <p class="text-muted"> - <small> - {% blocktrans trimmed %} - {{ number_of_filtered_projects }} of {{ number_of_projects }} projects shown - {% endblocktrans %} - </small> - </p> - <select name="catalog" id="catalog" class="form-control"> - <option value="">{% trans 'All catalogs' %}</option> - {% for catalog in catalogs %} - <option value="{{ catalog.id }}" {% if catalog.id|stringformat:'s' == request.GET.catalog %}selected{% endif %}> - {{ catalog.title }} - </option> - {% endfor %} - </select> - </form> - <script> - $(document).ready(function() { - $('#catalog').change(function() { - $('#projects-search').trigger('submit'); - }); - }); - </script> - - <h2>{% trans 'Options' %}</h2> - - <ul class="list-unstyled"> - <li> - <a href="{% url 'projects' %}">{% trans 'Back to my projects' %}</a> - </li> - </ul> - -{% endblock %} - -{% block page %} - <h1>{% blocktrans trimmed with site=request.site %}All projects on {{ site }}{% endblocktrans %}</h1> - - <table class="table projects-table"> - <thead> - <tr> - <th style="width: 40%;">{% trans 'Name' %}</th> - <th style="width: 10%;">{% trans 'Progress' %}</th> - <th style="width: 20%;">{% trans 'Created' %}</th> - <th style="width: 23%;">{% trans 'Last changed' %}</th> - <th style="width: 7%;"></th> - </tr> - </thead> - <tbody> - {% for project in projects %} - <tr> - <td> - <a class=""href="{% url 'project' project.pk %}"> - {% for ancestor in project.get_ancestors %}{{ ancestor }} / {% endfor %} - <strong>{{ project.title }}</strong> - </a> - </td> - <td> - {% project_progress project %} - </td> - <td> - {{ project.created }} - </td> - <td> - {{ project.last_changed }} - </td> - <td class="text-right"> - <a href="{% url 'project_update' project.pk %}?next={% url 'site_projects' %}" class="fa fa-pencil"></a> - <a href="{% url 'project_delete' project.pk %}?next={% url 'site_projects' %}" class="fa fa-trash"></a> - </td> - </tr> - {% endfor %} - </tbody> - </table> - - {% include 'projects/projects_pagination.html' %} -{% endblock %} diff --git a/rdmo/projects/tests/test_commands.py b/rdmo/projects/tests/test_commands.py index cdb0cd3450..e777e760b6 100644 --- a/rdmo/projects/tests/test_commands.py +++ b/rdmo/projects/tests/test_commands.py @@ -53,7 +53,7 @@ def test_prune_projects_output2(db, settings): call_command('prune_projects', stdout=stdout, stderr=stderr) assert stdout.getvalue() == \ - "Found projects without ['owner']:\n%s" % (get_prune_output(instances)) + f"Found projects without ['owner']:\n{get_prune_output(instances)}" assert not stderr.getvalue() @@ -65,7 +65,7 @@ def test_prune_projects_remove(db, settings): call_command('prune_projects', '--remove', stdout=stdout, stderr=stderr) std_output = stdout.getvalue() - prune_output = "Found projects without ['owner']:\n%s" % (get_prune_output(instances, True)) + prune_output = f"Found projects without ['owner']:\n{get_prune_output(instances, True)}" assert std_output == prune_output assert not stderr.getvalue() diff --git a/rdmo/projects/tests/test_navigation.py b/rdmo/projects/tests/test_navigation.py index 1e882e9552..6128a02e7f 100644 --- a/rdmo/projects/tests/test_navigation.py +++ b/rdmo/projects/tests/test_navigation.py @@ -13,33 +13,38 @@ # (count, total, show) for each page or section (as default fallback) result_map = { - 'http://example.com/terms/questions/catalog/individual': (1, 1, True), + 'http://example.com/terms/questions/catalog/individual': (8, 9), + 'http://example.com/terms/questions/catalog/individual/*': (1, 1, True), 'http://example.com/terms/questions/catalog/individual/autocomplete': (0, 1, True), - 'http://example.com/terms/questions/catalog/collections': (1, 1, True), + 'http://example.com/terms/questions/catalog/collections': (9, 10), + 'http://example.com/terms/questions/catalog/collections/*': (1, 1, True), 'http://example.com/terms/questions/catalog/collections/autocomplete': (0, 1, True), + 'http://example.com/terms/questions/catalog/set': (47, 57), 'http://example.com/terms/questions/catalog/set/individual-single': (8, 9, True), 'http://example.com/terms/questions/catalog/set/individual-collection': (9, 10, True), 'http://example.com/terms/questions/catalog/set/collection-single': (14, 18, True), 'http://example.com/terms/questions/catalog/set/collection-collection': (16, 20, True), + 'http://example.com/terms/questions/catalog/conditions': (7, 15), 'http://example.com/terms/questions/catalog/conditions/input': (2, 2, True), 'http://example.com/terms/questions/catalog/conditions/text_contains': (1, 1, True), - 'http://example.com/terms/questions/catalog/conditions/text_empty': (1, 1, False), + 'http://example.com/terms/questions/catalog/conditions/text_empty': (0, 0, False), 'http://example.com/terms/questions/catalog/conditions/text_equal': (1, 1, True), - 'http://example.com/terms/questions/catalog/conditions/text_greater_than': (1, 1, False), - 'http://example.com/terms/questions/catalog/conditions/text_greater_than_equal': (1, 1, False), - 'http://example.com/terms/questions/catalog/conditions/text_lesser_than': (1, 1, False), - 'http://example.com/terms/questions/catalog/conditions/text_lesser_than_equal': (1, 1, False), + 'http://example.com/terms/questions/catalog/conditions/text_greater_than': (0, 0, False), + 'http://example.com/terms/questions/catalog/conditions/text_greater_than_equal': (0, 0, False), + 'http://example.com/terms/questions/catalog/conditions/text_lesser_than': (0, 0, False), + 'http://example.com/terms/questions/catalog/conditions/text_lesser_than_equal': (0, 0, False), 'http://example.com/terms/questions/catalog/conditions/text_not_empty': (1, 1, True), - 'http://example.com/terms/questions/catalog/conditions/text_not_equal': (1, 1, False), - 'http://example.com/terms/questions/catalog/conditions/option_empty': (1, 1, False), + 'http://example.com/terms/questions/catalog/conditions/text_not_equal': (0, 0, False), + 'http://example.com/terms/questions/catalog/conditions/option_empty': (0, 0, False), 'http://example.com/terms/questions/catalog/conditions/option_equal': (1, 1, True), 'http://example.com/terms/questions/catalog/conditions/option_not_empty': (1, 1, True), - 'http://example.com/terms/questions/catalog/conditions/option_not_equal': (1, 1, False), + 'http://example.com/terms/questions/catalog/conditions/option_not_equal': (0, 0, False), 'http://example.com/terms/questions/catalog/conditions/set': (0, 2, True), 'http://example.com/terms/questions/catalog/conditions/set_set': (0, 2, True), 'http://example.com/terms/questions/catalog/conditions/optionset': (0, 2, True), 'http://example.com/terms/questions/catalog/conditions/text_set': (0, 2, True), - 'http://example.com/terms/questions/catalog/blocks/set': (9, 12, True), + 'http://example.com/terms/questions/catalog/blocks': (9, 18), + 'http://example.com/terms/questions/catalog/blocks/set': (9, 18, True), } @@ -54,14 +59,23 @@ def test_compute_navigation(db, section_uri): assert [item['id'] for item in navigation] == [element.id for element in project.catalog.elements] for section in navigation: + if section['uri'] in result_map: + count, total = result_map[section['uri']] + assert section['count'] == count, section['uri'] + assert section['total'] == total, section['uri'] + if 'pages' in section: for page in section['pages']: - if page['uri'] in result_map: - count, total, show = result_map[page['uri']] - elif section['uri'] in result_map: - count, total, show = result_map[section['uri']] + uri = page['uri'] + wildcard_uri = section['uri'] + '/*' + + if uri in result_map: + count, total, show = result_map[uri] + elif wildcard_uri in result_map: + count, total, show = result_map[wildcard_uri] else: raise AssertionError('{uri} not in result_map'.format(**page)) + assert page['count'] == count, page['uri'] assert page['total'] == total, page['uri'] assert page['show'] == show, page['uri'] diff --git a/rdmo/projects/tests/test_progress.py b/rdmo/projects/tests/test_progress.py index 31ccbdde36..d153df624b 100644 --- a/rdmo/projects/tests/test_progress.py +++ b/rdmo/projects/tests/test_progress.py @@ -6,8 +6,8 @@ projects = [1, 11] results_map = { - 1: (58, 81), - 11: (0, 29) + 1: (58, 94), + 11: (0, 36) } diff --git a/rdmo/projects/tests/test_validator_conflict.py b/rdmo/projects/tests/test_validator_conflict.py index e48191c6ce..c6b97761b4 100644 --- a/rdmo/projects/tests/test_validator_conflict.py +++ b/rdmo/projects/tests/test_validator_conflict.py @@ -13,11 +13,15 @@ def test_serializer_create(db): + value = Value.objects.get(project_id=project_id, snapshot=None, attribute__path=attribute_path) + + class MockedRequest: + data = {} + class MockedView: + request = MockedRequest() project = Project.objects.get(id=project_id) - value = Value.objects.get(project_id=project_id, snapshot=None, attribute__path=attribute_path) - validator = ValueConflictValidator() serializer = ValueSerializer() serializer.context['view'] = MockedView() @@ -31,11 +35,15 @@ class MockedView: def test_serializer_create_error(db): + value = Value.objects.get(project_id=project_id, snapshot=None, attribute__path=attribute_path) + + class MockedRequest: + data = {} + class MockedView: + request = MockedRequest() project = Project.objects.get(id=project_id) - value = Value.objects.get(project_id=project_id, snapshot=None, attribute__path=attribute_path) - validator = ValueConflictValidator() serializer = ValueSerializer() serializer.context['view'] = MockedView() @@ -121,3 +129,100 @@ class MockedView: 'set_index': value.set_index, 'collection_index': value.collection_index, }, serializer) + + +def test_serializer_create_checkbox(db): + value = Value.objects.get( + project_id=project_id, + snapshot=None, + attribute__path='individual/collection/checkbox', + collection_index=0 + ) + value2 = Value.objects.get( + project_id=project_id, + snapshot=None, + attribute__path='individual/collection/checkbox', + collection_index=2 + ) + + class MockedRequest: + data = { + 'widget_type': 'checkbox' + } + + class MockedView: + request = MockedRequest() + project = Project.objects.get(id=project_id) + + validator = ValueConflictValidator() + serializer = ValueSerializer() + serializer.context['view'] = MockedView() + + validator({ + 'attribute': value.attribute, + 'set_prefix': value.set_prefix, + 'set_index': value.set_index, + 'collection_index': value.collection_index, + 'option': value2.option + }, serializer) + + +def test_serializer_create_checkbox_error(db): + value = Value.objects.get( + project_id=project_id, + snapshot=None, + attribute__path='individual/collection/checkbox', + collection_index=0 + ) + + class MockedRequest: + data = { + 'widget_type': 'checkbox' + } + + class MockedView: + request = MockedRequest() + project = Project.objects.get(id=project_id) + + validator = ValueConflictValidator() + serializer = ValueSerializer() + serializer.context['view'] = MockedView() + + with pytest.raises(RestFameworkValidationError): + validator({ + 'attribute': value.attribute, + 'set_prefix': value.set_prefix, + 'set_index': value.set_index, + 'collection_index': value.collection_index, + 'option': value.option + }, serializer) + + +def test_serializer_create_checkbox_text(db): + value = Value.objects.get( + project_id=project_id, + snapshot=None, + attribute__path='individual/collection/checkbox', + collection_index=0 + ) + + class MockedRequest: + data = { + 'widget_type': 'text' + } + + class MockedView: + request = MockedRequest() + project = Project.objects.get(id=project_id) + + validator = ValueConflictValidator() + serializer = ValueSerializer() + serializer.context['view'] = MockedView() + + with pytest.raises(RestFameworkValidationError): + validator({ + 'attribute': value.attribute, + 'set_prefix': value.set_prefix, + 'set_index': value.set_index, + 'collection_index': value.collection_index + }, serializer) diff --git a/rdmo/projects/tests/test_validator_value_type.py b/rdmo/projects/tests/test_validator_value_type.py new file mode 100644 index 0000000000..88bf0a46e5 --- /dev/null +++ b/rdmo/projects/tests/test_validator_value_type.py @@ -0,0 +1,113 @@ +import pytest + +from rest_framework.exceptions import ValidationError as RestFameworkValidationError + +from ..validators import ValueTypeValidator + +data = ( + ('url', 'https://example.com'), + ('url', 'http://example.com'), + ('integer', '1'), + ('integer', '-1'), + ('integer', '+1'), + ('integer', '12345'), + ('float', '1'), + ('float', '1.0'), + ('float', '+1.0'), + ('float', '-1.0'), + ('float', '1,000,000.12345'), + ('float', '1,0'), + ('float', '1.000.000,12345'), + ('float', '1.0e20'), + ('float', '1.0E20'), + ('float', '1.0e-20'), + ('float', '1.0e+20'), + ('boolean', '0'), + ('boolean', '1'), + ('boolean', 'f'), + ('boolean', 't'), + ('boolean', 'TrUe'), # spellchecker:disable-line + ('boolean', 'FaLsE'), # spellchecker:disable-line + ('boolean', 'true'), + ('boolean', 'false'), + ('date', '01.02.2024'), + ('date', '1.2.2024'), + ('date', '13.01.1337'), + ('date', '2/1/2024'), + ('date', '2024-01-02'), + ('date', '1. 2. 2024'), + ('datetime', '2024-01-02'), + ('datetime', '2024-01-02T10:00'), + ('datetime', '2024-01-02T10:00:00'), + ('datetime', '2024-01-02T10:00:00.123'), + ('datetime', '2024-01-02T10:00:00+02:00'), + ('email', 'user@example.com'), + ('email', 'user+test@example.com'), + ('email', 'user!test@example.com'), + ('email', 'user.name@example.com'), + ('email', 'user.name+tag@example.com'), + ('phone', '123456'), + ('phone', '123 456'), + ('phone', '362 123456'), + ('phone', '(362) 123456'), + ('phone', '+49 (0) 362123456'), + ('phone', '+49 (0) 362 123456'), +) +data_error = ( + ('url', 'wrong'), + ('url', 'example.com'), + ('integer', 'wrong'), + ('integer', '1.0'), + ('integer', '1b'), + ('float', 'wrong'), + ('float', '1,0000.12456'), + ('float', '1.0000,12456'), + ('float', '1.0a20'), + ('boolean', 'wrong'), + ('boolean', '2'), + ('boolean', '-1'), + ('boolean', 'tr'), + ('boolean', 'truee'), + ('boolean', 'falze'), + ('date', 'wrong'), + ('date', '001.02.2024'), + ('date', '01.02.20240'), + ('date', '1,2.2024'), + ('date', '2-1-2024'), + ('date', '2024-001-02'), + ('date', '20240-01-02'), + ('date', '2024-1-2'), + ('datetime', 'wrong'), + ('datetime', '2024-13-02'), + ('datetime', '2024-13-02Y10:00:00'), + ('datetime', '2024-01-02T10:00:00ZZ+02:00'), + ('datetime', '2024-01-02T25:00'), + ('datetime', '2024-01-02T10:60:00'), + ('email', 'wrong'), + ('email', 'example.com'), + ('email', 'üser@example.com'), + ('email', 'user@test@example.com'), + ('email', 'user@com'), + ('email', 'user@.com'), + ('phone', 'wrong'), + ('phone', '123456a'), + ('phone', '123 456 a'), + ('phone', '362s 123456'), + ('phone', '(3 62) 123456'), + ('phone', '-49 (0) 362123456'), + ('phone', '49 (0) 362 123456'), + ('phone', '1234 (0) 123456'), +) + + +@pytest.mark.parametrize('value_type,text', data) +def test_serializer(db, value_type, text): + validator = ValueTypeValidator() + validator({'value_type': value_type, 'text': text}) + + +@pytest.mark.parametrize('value_type,text', data_error) +def test_serializer_error(db, value_type, text): + validator = ValueTypeValidator() + with pytest.raises(RestFameworkValidationError): + validator({'value_type': value_type, 'text': text}) diff --git a/rdmo/projects/tests/test_view_issue.py b/rdmo/projects/tests/test_view_issue.py index e80a67b4a2..8780c7114f 100644 --- a/rdmo/projects/tests/test_view_issue.py +++ b/rdmo/projects/tests/test_view_issue.py @@ -179,7 +179,7 @@ def test_issue_send_post_email(db, client, username, password, project_id, issue @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('issue_id', issues) @pytest.mark.parametrize('project_id', projects) -def test_issue_send_post_attachements(db, client, files, username, password, project_id, issue_id): +def test_issue_send_post_attachments(db, client, files, username, password, project_id, issue_id): client.login(username=username, password=password) issue = Issue.objects.filter(project_id=project_id, id=issue_id).first() diff --git a/rdmo/projects/tests/test_view_membership_multisite.py b/rdmo/projects/tests/test_view_membership_multisite.py index 6bf58c3af4..cda01aa247 100644 --- a/rdmo/projects/tests/test_view_membership_multisite.py +++ b/rdmo/projects/tests/test_view_membership_multisite.py @@ -32,8 +32,8 @@ sites_domains = ('example.com', 'foo.com', 'bar.com') -@pytest.fixture() -def multisite(settings): +@pytest.fixture +def _multisite(settings): settings.MULTISITE = True @@ -41,8 +41,9 @@ def multisite(settings): @pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('membership_role', membership_roles) @pytest.mark.parametrize('site_domain', sites_domains) +@pytest.mark.usefixtures("_multisite") def test_get_invite_email_project_path_function(db, client, username, password, project_id, - membership_role, site_domain, multisite): + membership_role, site_domain): client.login(username=username, password=password) current_site = Site.objects.get_current() @@ -69,8 +70,9 @@ def test_get_invite_email_project_path_function(db, client, username, password, @pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('membership_role', membership_roles) @pytest.mark.parametrize('site_domain', sites_domains) +@pytest.mark.usefixtures("_multisite") def test_invite_email_project_path_email_body(db, client, username, password, project_id, - membership_role, site_domain, multisite): + membership_role, site_domain): client.login(username=username, password=password) current_site = Site.objects.get_current() diff --git a/rdmo/projects/tests/test_view_project.py b/rdmo/projects/tests/test_view_project.py index 6f277ed987..b43660cf14 100644 --- a/rdmo/projects/tests/test_view_project.py +++ b/rdmo/projects/tests/test_view_project.py @@ -5,8 +5,6 @@ from django.contrib.auth.models import Group, User from django.urls import reverse -from pytest_django.asserts import assertContains, assertNotContains, assertTemplateUsed - from rdmo.questions.models import Catalog from rdmo.views.models import View @@ -75,39 +73,17 @@ def test_list(db, client, username, password): if password: assert response.status_code == 200 - assertTemplateUsed(response, 'projects/projects.html') + # assertTemplateUsed(response, 'projects/projects.html') if username in ('site', 'api'): assert projects == [] - assert response.context['number_of_projects'] == len([]) - assertContains(response, 'View all projects on') - else: - user_projects_map = view_project_permission_map.get(username, []) - assert sorted(set(map(int, projects))) == user_projects_map - assert response.context['number_of_projects'] == len(user_projects_map) - assertNotContains(response, 'View all projects on') - else: - assert response.status_code == 302 - - -@pytest.mark.parametrize('username,password', users) -def test_site(db, client, username, password): - client.login(username=username, password=password) - - url = reverse('site_projects') - response = client.get(url) - - projects = re.findall(r'/projects/(\d+)/update/', response.content.decode()) - - if password: - if username in ('site', 'api'): - assert response.status_code == 200 - assertTemplateUsed(response, 'projects/site_projects.html') - user_projects_map = view_project_permission_map.get(username, []) - assert sorted(set(map(int, projects))) == user_projects_map - assert response.context['number_of_projects'] == len(user_projects_map) + # assert response.context['number_of_projects'] == len([]) + # assertContains(response, 'View all projects on') else: - assert response.status_code == 403 + # user_projects_map = view_project_permission_map.get(username, []) + assert sorted(set(map(int, projects))) == [] + # assert response.context['number_of_projects'] == len(user_projects_map) + # assertNotContains(response, 'View all projects on') else: assert response.status_code == 302 diff --git a/rdmo/projects/tests/test_view_project_create_import.py b/rdmo/projects/tests/test_view_project_create_import.py index f8af896dd5..123a7cb708 100644 --- a/rdmo/projects/tests/test_view_project_create_import.py +++ b/rdmo/projects/tests/test_view_project_create_import.py @@ -292,7 +292,7 @@ def test_project_create_import_post_import_file_cancel(db, settings, client, fil @pytest.mark.parametrize('username,password', users) -def test_project_create_import_post_import_empty(db, settings, client, username, password): +def test_project_create_import_post_import_empty(db, settings, files, client, username, password): client.login(username=username, password=password) projects_count = Project.objects.count() diff --git a/rdmo/projects/tests/test_view_project_join.py b/rdmo/projects/tests/test_view_project_join.py index ecde517d49..4ecd2371e5 100644 --- a/rdmo/projects/tests/test_view_project_join.py +++ b/rdmo/projects/tests/test_view_project_join.py @@ -8,8 +8,8 @@ membership_roles = ('owner', 'manager', 'author', 'guest') -@pytest.fixture() -def use_project_invite_timeout(settings): +@pytest.fixture +def _use_project_invite_timeout(settings): settings.PROJECT_INVITE_TIMEOUT = -1 @@ -85,7 +85,8 @@ def test_project_join_error(db, client, membership_role): @pytest.mark.parametrize('membership_role', membership_roles) -def test_project_join_timeout_error(db, client, membership_role, use_project_invite_timeout): +@pytest.mark.usefixtures("_use_project_invite_timeout") +def test_project_join_timeout_error(db, client, membership_role): client.login(username='user', password='user') project = Project.objects.get(id=1) diff --git a/rdmo/projects/tests/test_view_project_update_import.py b/rdmo/projects/tests/test_view_project_update_import.py index d2da3816b3..6f99bd8331 100644 --- a/rdmo/projects/tests/test_view_project_update_import.py +++ b/rdmo/projects/tests/test_view_project_update_import.py @@ -285,7 +285,7 @@ def test_project_update_import_post_import_file_cancel(db, settings, client, fil @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -def test_project_update_import_post_import_file_empty(db, settings, client, username, password, project_id): +def test_project_update_import_post_import_file_empty(db, settings, files, client, username, password, project_id): client.login(username=username, password=password) projects_count = Project.objects.count() @@ -368,7 +368,7 @@ def test_project_update_import_post_import_project_step1(db, settings, client, u @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('source_id', projects) -def test_project_update_import_post_import_project_step2(db, settings, client, username, password, +def test_project_update_import_post_import_project_step2(db, settings, files, client, username, password, project_id, source_id): client.login(username=username, password=password) projects_count = Project.objects.count() diff --git a/rdmo/projects/tests/test_view_snapshot.py b/rdmo/projects/tests/test_view_snapshot.py index a49b161d0f..4a378177a6 100644 --- a/rdmo/projects/tests/test_view_snapshot.py +++ b/rdmo/projects/tests/test_view_snapshot.py @@ -97,7 +97,7 @@ def test_snapshot_create_post(db, client, files, username, password, project_id) @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('snapshot_id', snapshots) -def test_snapshot_update_get(db, client, username, password, project_id, snapshot_id): +def test_snapshot_update_get(db, client, files, username, password, project_id, snapshot_id): client.login(username=username, password=password) project = Project.objects.get(pk=project_id) @@ -122,7 +122,7 @@ def test_snapshot_update_get(db, client, username, password, project_id, snapsho @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('snapshot_id', snapshots) -def test_snapshot_update_post(db, client, username, password, project_id, snapshot_id): +def test_snapshot_update_post(db, client, files, username, password, project_id, snapshot_id): client.login(username=username, password=password) project = Project.objects.get(pk=project_id) snapshot = Snapshot.objects.get(pk=snapshot_id) diff --git a/rdmo/projects/tests/test_viewset_catalog.py b/rdmo/projects/tests/test_viewset_catalog.py new file mode 100644 index 0000000000..f7a35d28e2 --- /dev/null +++ b/rdmo/projects/tests/test_viewset_catalog.py @@ -0,0 +1,34 @@ +import pytest + +from django.urls import reverse + +users = ( + ('owner', 'owner'), + ('manager', 'manager'), + ('author', 'author'), + ('guest', 'guest'), + ('api', 'api'), + ('user', 'user'), + ('site', 'site'), + ('anonymous', None), +) + +urlnames = { + 'list': 'v1-projects:catalog-list' +} + +catalog_id = 1 + + +@pytest.mark.parametrize('username,password', users) +def test_list(db, client, username, password): + client.login(username=username, password=password) + + url = reverse(urlnames['list']) + response = client.get(url) + + if password: + assert response.status_code == 200 + assert isinstance(response.json(), list) + else: + assert response.status_code == 401 diff --git a/rdmo/projects/tests/test_viewset_invite.py b/rdmo/projects/tests/test_viewset_invite.py index e95b2c6533..096fb3f8be 100644 --- a/rdmo/projects/tests/test_viewset_invite.py +++ b/rdmo/projects/tests/test_viewset_invite.py @@ -26,7 +26,8 @@ urlnames = { 'list': 'v1-projects:invite-list', - 'detail': 'v1-projects:invite-detail' + 'detail': 'v1-projects:invite-detail', + 'user': 'v1-projects:invite-user' } invites = [1, 2] @@ -112,3 +113,22 @@ def test_delete(db, client, username, password, invite_id): assert response.status_code == 405 else: assert response.status_code == 401 + + +@pytest.mark.parametrize('username,password', users) +def test_user(db, client, username, password): + client.login(username=username, password=password) + + url = reverse(urlnames['user']) + response = client.get(url) + + if password: + assert response.status_code == 200 + assert isinstance(response.json(), list) + + if username == 'user': + assert sorted([item['id'] for item in response.json()]) == [2] + else: + assert sorted([item['id'] for item in response.json()]) == [] + else: + assert response.status_code == 401 diff --git a/rdmo/projects/tests/test_viewset_project.py b/rdmo/projects/tests/test_viewset_project.py index 8e9d8b6371..0069d25777 100644 --- a/rdmo/projects/tests/test_viewset_project.py +++ b/rdmo/projects/tests/test_viewset_project.py @@ -45,6 +45,8 @@ 'navigation': 'v1-projects:project-navigation', 'options': 'v1-projects:project-options', 'resolve': 'v1-projects:project-resolve', + 'upload_accept': 'v1-projects:project-upload-accept', + 'imports': 'v1-projects:project-imports' } projects = [1, 2, 3, 4, 5] @@ -59,6 +61,8 @@ project_id = 1 +page_size = 5 + @pytest.mark.parametrize('username,password', users) def test_list(db, client, username, password): client.login(username=username, password=password) @@ -67,15 +71,19 @@ def test_list(db, client, username, password): response = client.get(url) if password: + response_data = response.json() + assert response.status_code == 200 - assert isinstance(response.json(), list) + assert isinstance(response_data, dict) if username == 'user': - assert sorted([item['id'] for item in response.json()]) == [] + assert response_data['count'] == 0 + assert response_data['results'] == [] else: values_list = Project.objects.filter(id__in=view_project_permission_map.get(username, [])) \ - .order_by('id').values_list('id', flat=True) - assert sorted([item['id'] for item in response.json()]) == list(values_list) + .values_list('id', flat=True) + assert response_data['count'] == len(values_list) + assert [item['id'] for item in response_data['results']] == list(values_list[:page_size]) else: assert response.status_code == 401 @@ -359,3 +367,32 @@ def test_options_text_and_help(db, client, mocker): 'text_and_help': 'Simple answer 1' } ] + + +@pytest.mark.parametrize('username,password', users) +def test_upload_accept(db, client, username, password): + client.login(username=username, password=password) + + url = reverse(urlnames['upload_accept']) + response = client.get(url) + + if password: + assert response.status_code == 200 + assert response.json() == '.xml' + else: + assert response.status_code == 401 + + +@pytest.mark.parametrize('username,password', users) +def test_imports(db, client, username, password): + client.login(username=username, password=password) + + url = reverse(urlnames['imports']) + response = client.get(url) + + if password: + assert response.status_code == 200 + assert len(response.json()) == 1 + assert response.json()[0]['key'] == 'url' + else: + assert response.status_code == 401 diff --git a/rdmo/projects/tests/test_viewset_project_navigation.py b/rdmo/projects/tests/test_viewset_project_navigation.py new file mode 100644 index 0000000000..eb0c5a977d --- /dev/null +++ b/rdmo/projects/tests/test_viewset_project_navigation.py @@ -0,0 +1,68 @@ +import pytest + +from django.urls import reverse + +from ..models import Project + +users = ( + ('owner', 'owner'), + ('manager', 'manager'), + ('author', 'author'), + ('guest', 'guest'), + ('api', 'api'), + ('user', 'user'), + ('site', 'site'), + ('anonymous', None), +) + +view_progress_permission_map = { + 'owner': [1, 2, 3, 4, 5, 10], + 'manager': [1, 3, 5, 7], + 'author': [1, 3, 5, 8], + 'guest': [1, 3, 5, 9], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] +} + +change_progress_permission_map = { + 'owner': [1, 2, 3, 4, 5, 10], + 'manager': [1, 3, 5, 7], + 'author': [1, 3, 5, 8], + 'api': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + 'site': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] +} + +urlnames = { + 'navigation': 'v1-projects:project-navigation' +} + +projects = [1, 2, 3, 4, 5] +sections = [1] + + +@pytest.mark.parametrize('username,password', users) +@pytest.mark.parametrize('project_id', projects) +def test_navigation_get(db, client, username, password, project_id): + client.login(username=username, password=password) + + project = Project.objects.get(id=project_id) + sections = project.catalog.sections.order_by("section_catalogs").all() + + if project_id in view_progress_permission_map.get(username, []): + catalog_elements = project.catalog.elements + for section in sections: + url = reverse(urlnames['navigation'], args=[project_id, section.id]) + response = client.get(url) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(catalog_elements) == len(data) + + else: + if sections: + url = reverse(urlnames['navigation'], args=[project_id, sections[0].id]) + response = client.get(url) + if password: + assert response.status_code == 404 + else: + assert response.status_code == 401 diff --git a/rdmo/projects/tests/test_viewset_project_progress.py b/rdmo/projects/tests/test_viewset_project_progress.py index 6c39a52798..5d7529261f 100644 --- a/rdmo/projects/tests/test_viewset_project_progress.py +++ b/rdmo/projects/tests/test_viewset_project_progress.py @@ -91,3 +91,45 @@ def test_progress_post(db, client, username, password, project_id): assert response.status_code == 404 else: assert response.status_code == 401 + + +def test_progress_post_changed(db, client): + client.login(username='owner', password='owner') + + project = Project.objects.get(id=1) + project.progress_count = progress_count = 0 + project.progress_total = progress_total = 0 + project.save() + project.refresh_from_db() + project_updated = project.updated + + url = reverse(urlnames['progress'], args=[1]) + response = client.post(url) + + project.refresh_from_db() + + assert response.status_code == 200 + assert project.updated > project_updated + assert project.progress_count > progress_count + assert project.progress_total > progress_total + + +def test_progress_post_unchanged(db, client): + client.login(username='owner', password='owner') + + project = Project.objects.get(id=1) + project.progress_count = progress_count = 58 # the progress in the fixture is not up-to-date + project.progress_total = progress_total = 94 + project.save() + project.refresh_from_db() + project_updated = project.updated + + url = reverse(urlnames['progress'], args=[1]) + response = client.post(url) + + project.refresh_from_db() + + assert response.status_code == 200 + assert project.progress_count == progress_count + assert project.progress_total == progress_total + assert project.updated == project_updated diff --git a/rdmo/projects/tests/test_viewset_project_snapshot.py b/rdmo/projects/tests/test_viewset_project_snapshot.py index 2c3c7c7337..54a91c3f90 100644 --- a/rdmo/projects/tests/test_viewset_project_snapshot.py +++ b/rdmo/projects/tests/test_viewset_project_snapshot.py @@ -125,7 +125,7 @@ def test_create(db, client, files, username, password, project_id): @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('snapshot_id', snapshots) -def test_update(db, client, username, password, project_id, snapshot_id): +def test_update(db, client, files, username, password, project_id, snapshot_id): client.login(username=username, password=password) project = Project.objects.get(id=project_id) snapshot = Snapshot.objects.filter(project_id=project_id, id=snapshot_id).first() @@ -159,7 +159,7 @@ def test_update(db, client, username, password, project_id, snapshot_id): @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) @pytest.mark.parametrize('snapshot_id', snapshots) -def test_delete(db, client, username, password, project_id, snapshot_id): +def test_delete(db, client, files, username, password, project_id, snapshot_id): client.login(username=username, password=password) project = Project.objects.get(id=project_id) diff --git a/rdmo/projects/tests/test_viewset_project_value.py b/rdmo/projects/tests/test_viewset_project_value.py index ecccedb347..0b5afb6ed0 100644 --- a/rdmo/projects/tests/test_viewset_project_value.py +++ b/rdmo/projects/tests/test_viewset_project_value.py @@ -5,7 +5,7 @@ from django.conf import settings from django.urls import reverse -from rdmo.core.constants import VALUE_TYPE_CHOICES, VALUE_TYPE_FILE, VALUE_TYPE_TEXT +from rdmo.core.constants import VALUE_TYPE_FILE, VALUE_TYPE_TEXT from ..models import Value @@ -56,6 +56,16 @@ ] set_questionsets = [42, 43] +value_texts = ( + ('text', 'Lorem ipsum'), + ('url', 'https://lorem.ipsum'), + ('integer', '1337'), + ('float', '13.37'), + ('boolean', '1'), + ('datetime', '1337-01-13T13:37+13:37'), + ('email', 'user@lorem.ipsum'), + ('phone', '+49 (0) 1337 12345678') +) @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) @@ -101,8 +111,8 @@ def test_detail(db, client, username, password, project_id, value_id): @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -@pytest.mark.parametrize('value_type,value_type_label', VALUE_TYPE_CHOICES) -def test_create_text(db, client, username, password, project_id, value_type, value_type_label): +@pytest.mark.parametrize('value_type,value_text', value_texts) +def test_create_text(db, client, username, password, project_id, value_type, value_text): client.login(username=username, password=password) url = reverse(urlnames['list'], args=[project_id]) @@ -110,14 +120,14 @@ def test_create_text(db, client, username, password, project_id, value_type, val 'attribute': attribute_id, 'set_index': 0, 'collection_index': 0, - 'text': 'Lorem ipsum', + 'text': value_text, 'value_type': value_type, 'unit': '' } response = client.post(url, data) if project_id in add_value_permission_map.get(username, []): - assert response.status_code == 201 + assert response.status_code == 201, response.content assert isinstance(response.json(), dict) assert response.json().get('id') in Value.objects.filter(project_id=project_id).values_list('id', flat=True) elif project_id in view_value_permission_map.get(username, []): @@ -128,8 +138,8 @@ def test_create_text(db, client, username, password, project_id, value_type, val @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -@pytest.mark.parametrize('value_type,value_type_label', VALUE_TYPE_CHOICES) -def test_create_option(db, client, username, password, project_id, value_type, value_type_label): +@pytest.mark.parametrize('value_type,value_text', value_texts) +def test_create_option(db, client, username, password, project_id, value_type, value_text): client.login(username=username, password=password) url = reverse(urlnames['list'], args=[project_id]) @@ -137,6 +147,7 @@ def test_create_option(db, client, username, password, project_id, value_type, v 'attribute': attribute_id, 'set_index': 0, 'collection_index': 0, + 'text': value_text, 'option': option_id, 'value_type': value_type, 'unit': '' @@ -155,8 +166,8 @@ def test_create_option(db, client, username, password, project_id, value_type, v @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -@pytest.mark.parametrize('value_type,value_type_label', VALUE_TYPE_CHOICES) -def test_create_external(db, client, username, password, project_id, value_type, value_type_label): +@pytest.mark.parametrize('value_type,value_text', value_texts) +def test_create_external(db, client, username, password, project_id, value_type, value_text): client.login(username=username, password=password) url = reverse(urlnames['list'], args=[project_id]) @@ -164,7 +175,7 @@ def test_create_external(db, client, username, password, project_id, value_type, 'attribute': attribute_id, 'set_index': 0, 'collection_index': 0, - 'text': 'Lorem ipsum', + 'text': value_text, 'external_id': '1', 'value_type': value_type, 'unit': '' @@ -232,7 +243,7 @@ def test_delete(db, client, username, password, project_id, value_id): @pytest.mark.parametrize('username,password', users) @pytest.mark.parametrize('project_id', projects) -@pytest.mark.parametrize('value_id, set_values_count', set_values) +@pytest.mark.parametrize('value_id,set_values_count', set_values) def test_set(db, client, username, password, project_id, value_id, set_values_count): client.login(username=username, password=password) value_exists = Value.objects.filter(project_id=project_id, snapshot=None, id=value_id).exists() diff --git a/rdmo/projects/urls/__init__.py b/rdmo/projects/urls/__init__.py index b71fb82221..47472eeeaf 100644 --- a/rdmo/projects/urls/__init__.py +++ b/rdmo/projects/urls/__init__.py @@ -34,7 +34,6 @@ ProjectUpdateViewsView, ProjectViewExportView, ProjectViewView, - SiteProjectsView, SnapshotCreateView, SnapshotRollbackView, SnapshotUpdateView, @@ -43,9 +42,6 @@ urlpatterns = [ re_path(r'^$', ProjectsView.as_view(), name='projects'), - re_path(r'^all/$', - SiteProjectsView.as_view(), name='site_projects'), - re_path(r'^create/$', ProjectCreateView.as_view(), name='project_create'), re_path(r'^join/(?P<token>.+)/$', diff --git a/rdmo/projects/urls/v1.py b/rdmo/projects/urls/v1.py index 8f8f373a3d..a3ab70fa9d 100644 --- a/rdmo/projects/urls/v1.py +++ b/rdmo/projects/urls/v1.py @@ -3,6 +3,7 @@ from rest_framework_extensions.routers import ExtendedDefaultRouter from ..viewsets import ( + CatalogViewSet, IntegrationViewSet, InviteViewSet, IssueViewSet, @@ -43,7 +44,7 @@ router.register(r'issues', IssueViewSet, basename='issue') router.register(r'snapshots', SnapshotViewSet, basename='snapshot') router.register(r'values', ValueViewSet, basename='value') - +router.register(r'catalogs', CatalogViewSet, basename='catalog') urlpatterns = [ path('', include(router.urls)), diff --git a/rdmo/projects/validators.py b/rdmo/projects/validators.py index 77c7495f3f..da0ace2423 100644 --- a/rdmo/projects/validators.py +++ b/rdmo/projects/validators.py @@ -1,13 +1,24 @@ -from datetime import timedelta +from datetime import datetime, timedelta from django.conf import settings -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError +from django.core.validators import EmailValidator, RegexValidator, URLValidator from django.utils.dateparse import parse_datetime from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from rdmo.core.constants import VALUE_TYPE_FILE +from rdmo.core.constants import ( + VALUE_TYPE_BOOLEAN, + VALUE_TYPE_DATE, + VALUE_TYPE_DATETIME, + VALUE_TYPE_EMAIL, + VALUE_TYPE_FILE, + VALUE_TYPE_FLOAT, + VALUE_TYPE_INTEGER, + VALUE_TYPE_PHONE, + VALUE_TYPE_URL, +) from rdmo.core.utils import human2bytes @@ -28,13 +39,20 @@ def __call__(self, data, serializer): }) else: # for a new value, check if there is already a value with the same attribute and indexes + get_kwargs = { + 'attribute': data.get('attribute'), + 'set_prefix': data.get('set_prefix'), + 'set_index': data.get('set_index'), + 'collection_index': data.get('collection_index') + } + + # for checkboxes, check only values with the same option. the widget_type is provided with the post request + widget_type = serializer.context['view'].request.data.get('widget_type') + if widget_type == 'checkbox': + get_kwargs['option'] = data.get('option') + try: - serializer.context['view'].project.values.filter(snapshot=None).get( - attribute=data.get('attribute'), - set_prefix=data.get('set_prefix'), - set_index=data.get('set_index'), - collection_index=data.get('collection_index') - ) + serializer.context['view'].project.values.filter(snapshot=None).get(**get_kwargs) except ObjectDoesNotExist: return except MultipleObjectsReturned: @@ -45,6 +63,7 @@ def __call__(self, data, serializer): ' was found.')] }) + class ValueQuotaValidator: requires_context = True @@ -56,3 +75,61 @@ def __call__(self, data, serializer): raise serializers.ValidationError({ 'quota': [_('The file quota for this project has been reached.')] }) + + +class ValueTypeValidator: + + def __call__(self, data): + text = data.get('text') + value_type = data.get('value_type') + + try: + self.validate(text, value_type) + except ValidationError as e: + raise serializers.ValidationError({ + 'text': [e.message] + }) from e + + def validate(self, text, value_type): + if text and settings.PROJECT_VALUES_VALIDATION: + if value_type == VALUE_TYPE_URL and settings.PROJECT_VALUES_VALIDATION_URL: + URLValidator()(text) + + elif value_type == VALUE_TYPE_INTEGER and settings.PROJECT_VALUES_VALIDATION_INTEGER: + RegexValidator( + settings.PROJECT_VALUES_VALIDATION_INTEGER_REGEX, + settings.PROJECT_VALUES_VALIDATION_INTEGER_MESSAGE + )(text) + + elif value_type == VALUE_TYPE_FLOAT and settings.PROJECT_VALUES_VALIDATION_FLOAT: + RegexValidator( + settings.PROJECT_VALUES_VALIDATION_FLOAT_REGEX, + settings.PROJECT_VALUES_VALIDATION_FLOAT_MESSAGE + )(text) + + elif value_type == VALUE_TYPE_BOOLEAN and settings.PROJECT_VALUES_VALIDATION_BOOLEAN: + RegexValidator( + settings.PROJECT_VALUES_VALIDATION_BOOLEAN_REGEX, + settings.PROJECT_VALUES_VALIDATION_BOOLEAN_MESSAGE + )(text) + + elif value_type == VALUE_TYPE_DATE and settings.PROJECT_VALUES_VALIDATION_DATE: + RegexValidator( + settings.PROJECT_VALUES_VALIDATION_DATE_REGEX, + settings.PROJECT_VALUES_VALIDATION_DATE_MESSAGE + )(text) + + elif value_type == VALUE_TYPE_DATETIME and settings.PROJECT_VALUES_VALIDATION_DATETIME: + try: + datetime.fromisoformat(text) + except ValueError as e: + raise ValidationError(_('Enter a valid datetime.')) from e + + elif value_type == VALUE_TYPE_EMAIL and settings.PROJECT_VALUES_VALIDATION_EMAIL: + EmailValidator()(text) + + elif value_type == VALUE_TYPE_PHONE and settings.PROJECT_VALUES_VALIDATION_PHONE: + RegexValidator( + settings.PROJECT_VALUES_VALIDATION_PHONE_REGEX, + settings.PROJECT_VALUES_VALIDATION_PHONE_MESSAGE + )(text) diff --git a/rdmo/projects/views/__init__.py b/rdmo/projects/views/__init__.py index 063dc97280..f42a45b5ab 100644 --- a/rdmo/projects/views/__init__.py +++ b/rdmo/projects/views/__init__.py @@ -12,7 +12,6 @@ ProjectLeaveView, ProjectQuestionsView, ProjectsView, - SiteProjectsView, ) from .project_answers import ProjectAnswersExportView, ProjectAnswersView from .project_create import ProjectCreateImportView, ProjectCreateView diff --git a/rdmo/projects/views/project.py b/rdmo/projects/views/project.py index e77373d280..88fc5fc99a 100644 --- a/rdmo/projects/views/project.py +++ b/rdmo/projects/views/project.py @@ -1,10 +1,8 @@ import logging +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import PermissionDenied -from django.db import models from django.db.models import F, OuterRef, Subquery -from django.db.models.functions import Coalesce, Greatest from django.forms import Form from django.http import Http404 from django.shortcuts import get_object_or_404, redirect @@ -15,101 +13,21 @@ from django.views.generic import DeleteView, DetailView, TemplateView from django.views.generic.edit import FormMixin -from django_filters.views import FilterView - -from rdmo.accounts.utils import is_site_manager from rdmo.core.plugins import get_plugin, get_plugins -from rdmo.core.views import ObjectPermissionMixin, RedirectViewMixin +from rdmo.core.views import CSRFViewMixin, ObjectPermissionMixin, RedirectViewMixin, StoreIdViewMixin from rdmo.questions.models import Catalog from rdmo.questions.utils import get_widgets from rdmo.tasks.models import Task from rdmo.views.models import View -from ..filters import ProjectFilter -from ..models import Integration, Invite, Membership, Project, Value -from ..utils import get_upload_accept, set_context_querystring_with_filter_and_page +from ..models import Integration, Invite, Membership, Project +from ..utils import get_upload_accept logger = logging.getLogger(__name__) -class ProjectsView(LoginRequiredMixin, FilterView): +class ProjectsView(LoginRequiredMixin, CSRFViewMixin, StoreIdViewMixin, TemplateView): template_name = 'projects/projects.html' - context_object_name = 'projects' - paginate_by = 20 - filterset_class = ProjectFilter - - def get_queryset(self): - # prepare projects queryset for this user - queryset = Project.objects.filter(user=self.request.user) - for instance in queryset: - queryset |= instance.get_descendants() - queryset = queryset.distinct() - - # prepare subquery for role - membership_subquery = models.Subquery( - Membership.objects.filter(project=models.OuterRef('pk'), user=self.request.user).values('role') - ) - queryset = queryset.annotate(role=membership_subquery) - - # prepare subquery for last_changed - last_changed_subquery = models.Subquery( - Value.objects.filter(project=models.OuterRef('pk')).order_by('-updated').values('updated')[:1] - ) - # the 'updated' field from a Project always returns a valid DateTime value - # when Greatest returns null, then Coalesce will return the value for 'updated' as a fall-back - # when Greatest returns a value, then Coalesce will return this value - queryset = queryset.annotate(last_changed=Coalesce(Greatest(last_changed_subquery, 'updated'), 'updated')) - - # order by last changed - queryset = queryset.order_by('-last_changed') - - return queryset - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['number_of_projects'] = self.get_queryset().count() - context['invites'] = Invite.objects.filter(user=self.request.user) - context['is_site_manager'] = is_site_manager(self.request.user) - context['number_of_filtered_projects'] = context["filter"].qs.count() - context['upload_accept'] = get_upload_accept() - context = set_context_querystring_with_filter_and_page(context) - return context - - -class SiteProjectsView(LoginRequiredMixin, FilterView): - template_name = 'projects/site_projects.html' - context_object_name = 'projects' - paginate_by = 20 - filterset_class = ProjectFilter - model = Project - - def get_queryset(self): - if is_site_manager(self.request.user): - # prepare projects queryset for the site manager - queryset = Project.objects.filter_current_site() - - # prepare subquery for last_changed - last_changed_subquery = models.Subquery( - Value.objects.filter(project=models.OuterRef('pk')).order_by('-updated').values('updated')[:1] - ) - # the 'updated' field from a Project always returns a valid DateTime value - # when Greatest returns null, then Coalesce will return the value for 'updated' as a fall-back - # when Greatest returns a value, then Coalesce will return this value - queryset = queryset.annotate(last_changed=Coalesce(Greatest(last_changed_subquery, 'updated'), 'updated')) - - return queryset - else: - raise PermissionDenied() - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['number_of_projects'] = self.get_queryset().count() - context['number_of_filtered_projects'] = context["filter"].qs.count() - context = set_context_querystring_with_filter_and_page(context) - context['catalogs'] = Catalog.objects.filter_current_site() \ - .filter_group(self.request.user) \ - .filter_availability(self.request.user) - return context class ProjectDetailView(ObjectPermissionMixin, DetailView): @@ -138,6 +56,10 @@ def get_context_data(self, **kwargs): .filter(highest=F('project__level')) \ .select_related('user') + if settings.SOCIALACCOUNT: + # prefetch the users social account, if that relation exists + memberships = memberships.prefetch_related('user__socialaccount_set') + integrations = Integration.objects.filter(project__in=ancestors) context['catalogs'] = Catalog.objects.filter_current_site() \ .filter_group(self.request.user) \ @@ -187,7 +109,7 @@ def get(self, request, token): error = _('Sorry, your invitation has been expired.') invite.delete() elif invite.user and invite.user != request.user: - error = _('Sorry, but this invitation is for the user "%s".' % invite.user) + error = _('Sorry, but this invitation is for the user "%s".') % invite.user elif Membership.objects.filter(project=invite.project, user=request.user).exists(): invite.delete() return redirect(invite.project.get_absolute_url()) diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py index 11b9c296c6..3ca669e769 100644 --- a/rdmo/projects/viewsets.py +++ b/rdmo/projects/viewsets.py @@ -1,6 +1,8 @@ from django.conf import settings from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ObjectDoesNotExist +from django.db.models import OuterRef, Prefetch, Subquery +from django.db.models.functions import Coalesce, Greatest from django.http import Http404, HttpResponseRedirect from django.utils.translation import gettext_lazy as _ @@ -8,6 +10,8 @@ from rest_framework.decorators import action from rest_framework.exceptions import NotFound from rest_framework.mixins import CreateModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin +from rest_framework.pagination import PageNumberPagination +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet @@ -19,11 +23,17 @@ from rdmo.core.permissions import HasModelPermission from rdmo.core.utils import human2bytes, return_file_response from rdmo.options.models import OptionSet -from rdmo.questions.models import Page, Question, QuestionSet +from rdmo.questions.models import Catalog, Page, Question, QuestionSet from rdmo.tasks.models import Task from rdmo.views.models import View -from .filters import SnapshotFilterBackend, ValueFilterBackend +from .filters import ( + ProjectDateFilterBackend, + ProjectOrderingFilter, + ProjectSearchFilterBackend, + SnapshotFilterBackend, + ValueFilterBackend, +) from .models import Continuation, Integration, Invite, Issue, Membership, Project, Snapshot, Value from .permissions import ( HasProjectPagePermission, @@ -48,18 +58,29 @@ ProjectSnapshotSerializer, ProjectValueSerializer, SnapshotSerializer, + UserInviteSerializer, ValueSerializer, ) -from .serializers.v1.overview import ProjectOverviewSerializer +from .serializers.v1.overview import CatalogSerializer, ProjectOverviewSerializer from .serializers.v1.page import PageSerializer -from .utils import check_conditions, send_invite_email +from .utils import check_conditions, get_upload_accept, send_invite_email + + +class ProjectPagination(PageNumberPagination): + page_size = settings.PROJECT_TABLE_PAGE_SIZE class ProjectViewSet(ModelViewSet): permission_classes = (HasModelPermission | HasProjectsPermission, ) serializer_class = ProjectSerializer + pagination_class = ProjectPagination - filter_backends = (DjangoFilterBackend,) + filter_backends = ( + DjangoFilterBackend, + ProjectDateFilterBackend, + ProjectOrderingFilter, + ProjectSearchFilterBackend, + ) filterset_fields = ( 'title', 'user', @@ -67,9 +88,33 @@ class ProjectViewSet(ModelViewSet): 'catalog', 'catalog__uri' ) + ordering_fields = ( + 'title', + 'progress', + 'role', + 'owner', + 'updated', + 'created', + 'last_changed' + ) def get_queryset(self): - return Project.objects.filter_user(self.request.user).select_related('catalog') + queryset = Project.objects.filter_user(self.request.user).distinct().prefetch_related( + 'snapshots', + 'views', + Prefetch('memberships', queryset=Membership.objects.select_related('user'), to_attr='memberships_list') + ).select_related('catalog') + + # prepare subquery for last_changed + last_changed_subquery = Subquery( + Value.objects.filter(project=OuterRef('pk')).order_by('-updated').values('updated')[:1] + ) + # the 'updated' field from a Project always returns a valid DateTime value + # when Greatest returns null, then Coalesce will return the value for 'updated' as a fall-back + # when Greatest returns a value, then Coalesce will return this value + queryset = queryset.annotate(last_changed=Coalesce(Greatest(last_changed_subquery, 'updated'), 'updated')) + + return queryset @action(detail=True, permission_classes=(HasModelPermission | HasProjectPermission, )) def overview(self, request, pk=None): @@ -166,7 +211,8 @@ def options(self, request, pk=None): if Question.objects.filter_by_catalog(project.catalog).filter(optionsets=optionset) and \ optionset.provider is not None: options = [] - for option in optionset.provider.get_options(project, search=request.GET.get('search')): + for option in optionset.provider.get_options(project, search=request.GET.get('search'), + user=request.user, site=request.site): if 'id' not in option: raise RuntimeError(f"'id' is missing in options of '{optionset.provider.class_name}'") elif 'text' not in option: @@ -192,11 +238,12 @@ def progress(self, request, pk=None): project = self.get_object() if request.method == 'POST' or project.progress_count is None or project.progress_total is None: - # compute the progress and store + # compute the progress, but store it only, if it has changed project.catalog.prefetch_elements() - project.progress_count, project.progress_total = compute_progress(project) - project.save() - + progress_count, progress_total = compute_progress(project) + if progress_count != project.progress_count or progress_total != project.progress_total: + project.progress_count, project.progress_total = progress_count, progress_total + project.save() try: ratio = project.progress_count / project.progress_total except ZeroDivisionError: @@ -208,6 +255,19 @@ def progress(self, request, pk=None): 'ratio': ratio }) + @action(detail=False, url_path='upload-accept', permission_classes=(IsAuthenticated, )) + def upload_accept(self, request): + return Response(get_upload_accept()) + + @action(detail=False, permission_classes=(IsAuthenticated, )) + def imports(self, request): + return Response([{ + 'key': key, + 'label': label, + 'class_name': class_name, + 'href': reverse('project_create_import', args=[key]) + } for key, label, class_name in settings.PROJECT_IMPORTS if key in settings.PROJECT_IMPORTS_LIST] ) + def perform_create(self, serializer): project = serializer.save(site=get_current_site(self.request)) @@ -548,6 +608,11 @@ def get_queryset(self): def get_detail_permission_object(self, obj): return obj.project + @action(detail=False, permission_classes=(IsAuthenticated, )) + def user(self, request): + invites = Invite.objects.filter(user=self.request.user) + serializer = UserInviteSerializer(invites, many=True) + return Response(serializer.data) class IssueViewSet(ReadOnlyModelViewSet): permission_classes = (HasModelPermission | HasProjectsPermission, ) @@ -604,3 +669,15 @@ def file(self, request, pk=None): # if it didn't work return 404 raise NotFound() + + +class CatalogViewSet(ListModelMixin, GenericViewSet): + permission_classes = (IsAuthenticated, ) + + serializer_class = CatalogSerializer + + def get_queryset(self): + return Catalog.objects.filter_current_site() \ + .filter_group(self.request.user) \ + .filter_availability(self.request.user) \ + .order_by('-available', 'order') diff --git a/rdmo/questions/imports.py b/rdmo/questions/imports.py index a4b062d616..c923f4e78f 100644 --- a/rdmo/questions/imports.py +++ b/rdmo/questions/imports.py @@ -1,20 +1,8 @@ -import logging - -from django.contrib.sites.models import Site - -from rdmo.core.imports import ( - check_permissions, - set_common_fields, - set_foreign_field, - set_lang_field, - set_m2m_instances, - set_m2m_through_instances, - set_reverse_m2m_through_instance, - validate_instance, -) +from rdmo.core.import_helpers import ElementImportHelper, ExtraFieldHelper, ThroughInstanceMapper +from ..core.constants import VALUE_TYPE_TEXT from .models import Catalog, Page, Question, QuestionSet, Section -from .utils import get_widget_types +from .utils import get_widget_type_or_default from .validators import ( CatalogLockedValidator, CatalogUniqueURIValidator, @@ -28,199 +16,126 @@ SectionUniqueURIValidator, ) -logger = logging.getLogger(__name__) - - -def import_catalog(element, save=False, user=None): - try: - catalog = Catalog.objects.get(uri=element.get('uri')) - except Catalog.DoesNotExist: - catalog = Catalog() - - set_common_fields(catalog, element) - - catalog.order = element.get('order') or 0 - - set_lang_field(catalog, 'title', element) - set_lang_field(catalog, 'help', element) - - catalog.available = element.get('available', True) - - validate_instance(catalog, element, CatalogLockedValidator, CatalogUniqueURIValidator) - - check_permissions(catalog, element, user) - - if save and not element.get('errors'): - if catalog.id: - element['updated'] = True - logger.info('Catalog %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('Catalog created with uri %s.', element.get('uri')) - - catalog.save() - set_m2m_through_instances(catalog, 'sections', element, 'catalog', 'section', 'catalog_sections') - catalog.sites.add(Site.objects.get_current()) - catalog.editors.add(Site.objects.get_current()) - - return catalog - - -def import_section(element, save=False, user=None): - try: - section = Section.objects.get(uri=element.get('uri')) - except Section.DoesNotExist: - section = Section() - - set_common_fields(section, element) - - set_lang_field(section, 'title', element) - - validate_instance(section, element, SectionLockedValidator, SectionUniqueURIValidator) - - check_permissions(section, element, user) - - if save and not element.get('errors'): - if section.id: - element['updated'] = True - logger.info('Section %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('Section created with uri %s.', element.get('uri')) - - section.save() - set_reverse_m2m_through_instance(section, 'catalog', element, 'section', 'catalog', 'section_catalogs') - set_m2m_through_instances(section, 'pages', element, 'section', 'page', 'section_pages') - section.editors.add(Site.objects.get_current()) - - return section - - -def import_page(element, save=False, user=None): - try: - page = Page.objects.get(uri=element.get('uri')) - except Page.DoesNotExist: - page = Page() - - set_common_fields(page, element) - set_foreign_field(page, 'attribute', element) - - page.is_collection = element.get('is_collection') or False - - set_lang_field(page, 'title', element) - set_lang_field(page, 'help', element) - set_lang_field(page, 'verbose_name', element) - - validate_instance(page, element, PageLockedValidator, PageUniqueURIValidator) - - check_permissions(page, element, user) - - if save and not element.get('errors'): - if page.id: - element['updated'] = True - logger.info('QuestionSet %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('QuestionSet created with uri %s.', element.get('uri')) - - page.save() - set_m2m_instances(page, 'conditions', element) - set_reverse_m2m_through_instance(page, 'section', element, 'page', 'section', 'page_sections') - set_m2m_through_instances(page, 'questionsets', element, 'page', 'questionset', 'page_questionsets') - set_m2m_through_instances(page, 'questions', element, 'page', 'question', 'page_questions') - page.editors.add(Site.objects.get_current()) - - return page - - -def import_questionset(element, save=False, user=None): - try: - questionset = QuestionSet.objects.get(uri=element.get('uri')) - except QuestionSet.DoesNotExist: - questionset = QuestionSet() - - set_common_fields(questionset, element) - set_foreign_field(questionset, 'attribute', element) - - questionset.is_collection = element.get('is_collection') or False - - set_lang_field(questionset, 'title', element) - set_lang_field(questionset, 'help', element) - set_lang_field(questionset, 'verbose_name', element) - - validate_instance(questionset, element, QuestionSetLockedValidator, QuestionSetUniqueURIValidator) - - check_permissions(questionset, element, user) - - if save and not element.get('errors'): - if questionset.id: - element['updated'] = True - logger.info('QuestionSet %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('QuestionSet created with uri %s.', element.get('uri')) - - questionset.save() - set_m2m_instances(questionset, 'conditions', element) - set_reverse_m2m_through_instance(questionset, 'page', element, 'questionset', 'page', 'questionset_pages') - set_reverse_m2m_through_instance(questionset, 'questionset', element, 'questionset', 'parent', 'questionset_parents') # noqa: E501 - set_m2m_through_instances(questionset, 'questionsets', element, 'parent', 'questionset', 'questionset_questionsets') # noqa: E501 - set_m2m_through_instances(questionset, 'questions', element, 'questionset', 'question', 'questionset_questions') - questionset.editors.add(Site.objects.get_current()) - - return questionset - - -def import_question(element, save=False, user=None): - try: - question = Question.objects.get(uri=element.get('uri')) - except Question.DoesNotExist: - question = Question() - - set_common_fields(question, element) - set_foreign_field(question, 'attribute', element) - - question.is_collection = element.get('is_collection') or False - question.is_optional = element.get('is_optional') or False - - set_lang_field(question, 'text', element) - set_lang_field(question, 'help', element) - set_lang_field(question, 'default_text', element) - set_lang_field(question, 'verbose_name', element) - - set_foreign_field(question, 'default_option', element) - - question.default_external_id = element.get('default_external_id') or '' - - if element.get('widget_type') in get_widget_types(): - question.widget_type = element.get('widget_type') - else: - question.widget_type = 'text' - - question.value_type = element.get('value_type') or '' - question.maximum = element.get('maximum') - question.minimum = element.get('minimum') - question.step = element.get('step') - question.unit = element.get('unit') or '' - question.width = element.get('width') - - validate_instance(question, element, QuestionLockedValidator, QuestionUniqueURIValidator) +import_helper_catalog = ElementImportHelper( + model=Catalog, + validators=(CatalogLockedValidator, CatalogUniqueURIValidator), + lang_fields=('title', 'help'), + extra_fields=( + ExtraFieldHelper(field_name='order'), + ExtraFieldHelper(field_name='available', overwrite_in_element=True), + ), + m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='sections', source_name='catalog', + target_name='section', through_name='catalog_sections' + ) + ], + add_current_site_sites = True, +) - check_permissions(question, element, user) +import_helper_section = ElementImportHelper( + model=Section, + validators=(SectionLockedValidator, SectionUniqueURIValidator), + lang_fields=('title', 'short_title'), + m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='pages', source_name='section', + target_name='page', through_name='section_pages' + ) + ], + reverse_m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='catalog', source_name='section', + target_name='catalog', through_name='section_catalogs' + ) + ] +) - if save and not element.get('errors'): - if question.id: - element['updated'] = True - logger.info('Question %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('Question created with uri %s.', element.get('uri')) +import_helper_page = ElementImportHelper( + model=Page, + validators=(PageLockedValidator, PageUniqueURIValidator), + lang_fields=('help', 'title', 'verbose_name', 'short_title'), + foreign_fields=('attribute',), + extra_fields=( + ExtraFieldHelper(field_name='is_collection'), + ), + m2m_instance_fields=('conditions', ), + m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='questionsets', source_name='page', + target_name='questionset', through_name='page_questionsets' + ), + ThroughInstanceMapper( + field_name='questions', source_name='page', + target_name='question', through_name='page_questions' + ) + ], + reverse_m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='section', source_name='page', + target_name='section', through_name='page_sections' + ) + ] +) - question.save() - set_reverse_m2m_through_instance(question, 'page', element, 'question', 'page', 'question_pages') - set_reverse_m2m_through_instance(question, 'questionset', element, 'question', 'questionset', 'question_questionsets') # noqa: E501 - set_m2m_instances(question, 'conditions', element) - set_m2m_instances(question, 'optionsets', element) - question.editors.add(Site.objects.get_current()) +import_helper_questionset = ElementImportHelper( + model=QuestionSet, + validators=(QuestionSetLockedValidator, QuestionSetUniqueURIValidator), + lang_fields=( 'title', 'help', 'verbose_name'), + foreign_fields=('attribute',), + extra_fields=( + ExtraFieldHelper(field_name='is_collection'), + ), + m2m_instance_fields=('conditions', ), + + m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='questionsets', source_name='parent', + target_name='questionset', through_name='questionset_questionsets' + ), + ThroughInstanceMapper( + field_name='questions', source_name='questionset', + target_name='question', through_name='questionset_questions' + ) + ], + reverse_m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='page', source_name='questionset', + target_name='page', through_name='questionset_pages' + ), + ThroughInstanceMapper( + field_name='questionset', source_name='questionset', + target_name='parent', through_name='questionset_parents' + ) + ] +) - return question +import_helper_question = ElementImportHelper( + model=Question, + validators=(QuestionLockedValidator, QuestionUniqueURIValidator), + lang_fields=('text', 'help', 'default_text', 'verbose_name'), + foreign_fields=('attribute', 'default_option'), + extra_fields=( + ExtraFieldHelper(field_name='is_collection'), + ExtraFieldHelper(field_name='is_optional'), + ExtraFieldHelper(field_name='default_external_id', value=''), + ExtraFieldHelper(field_name='widget_type', callback=get_widget_type_or_default), + ExtraFieldHelper(field_name='value_type', value=VALUE_TYPE_TEXT), + ExtraFieldHelper(field_name='minimum'), + ExtraFieldHelper(field_name='maximum'), + ExtraFieldHelper(field_name='step'), + ExtraFieldHelper(field_name='unit', value=''), + ExtraFieldHelper(field_name='width'), + ), + m2m_instance_fields=('conditions', 'optionsets'), + reverse_m2m_through_instance_fields=[ + ThroughInstanceMapper( + field_name='page', source_name='question', + target_name='page', through_name='question_pages' + ), + ThroughInstanceMapper( + field_name='questionset', source_name='question', + target_name='questionset', through_name='question_questionsets' + ) + ] +) diff --git a/rdmo/questions/migrations/0094_section_short_title.py b/rdmo/questions/migrations/0094_section_short_title.py new file mode 100644 index 0000000000..7739742127 --- /dev/null +++ b/rdmo/questions/migrations/0094_section_short_title.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.8 on 2024-06-06 15:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questions', '0093_alter_help_text_and_set_default'), + ] + + operations = [ + migrations.AddField( + model_name='section', + name='short_title_lang1', + field=models.CharField(blank=True, help_text='The short title for this section (in the primary language), used in the navigation.', max_length=32, verbose_name='Short title (primary)'), + ), + migrations.AddField( + model_name='section', + name='short_title_lang2', + field=models.CharField(blank=True, help_text='The short title for this section (in the secondary language), used in the navigation.', max_length=32, verbose_name='Short title (secondary)'), + ), + migrations.AddField( + model_name='section', + name='short_title_lang3', + field=models.CharField(blank=True, help_text='The short title for this section (in the tertiary language), used in the navigation.', max_length=32, verbose_name='Short title (tertiary)'), + ), + migrations.AddField( + model_name='section', + name='short_title_lang4', + field=models.CharField(blank=True, help_text='The short title for this section (in the quaternary language), used in the navigation.', max_length=32, verbose_name='Short title (quaternary)'), + ), + migrations.AddField( + model_name='section', + name='short_title_lang5', + field=models.CharField(blank=True, help_text='The short title for this section (in the quinary language), used in the navigation.', max_length=32, verbose_name='Short title (quinary)'), + ), + ] diff --git a/rdmo/questions/migrations/0095_page_short_title.py b/rdmo/questions/migrations/0095_page_short_title.py new file mode 100644 index 0000000000..cf257c3df7 --- /dev/null +++ b/rdmo/questions/migrations/0095_page_short_title.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.8 on 2024-06-06 15:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questions', '0094_section_short_title'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='short_title_lang1', + field=models.CharField(blank=True, help_text='The short title for this page (in the primary language), used in the navigation.', max_length=32, verbose_name='Short title (primary)'), + ), + migrations.AddField( + model_name='page', + name='short_title_lang2', + field=models.CharField(blank=True, help_text='The short title for this page (in the secondary language), used in the navigation.', max_length=32, verbose_name='Short title (secondary)'), + ), + migrations.AddField( + model_name='page', + name='short_title_lang3', + field=models.CharField(blank=True, help_text='The short title for this page (in the tertiary language), used in the navigation.', max_length=32, verbose_name='Short title (tertiary)'), + ), + migrations.AddField( + model_name='page', + name='short_title_lang4', + field=models.CharField(blank=True, help_text='The short title for this page (in the quaternary language), used in the navigation.', max_length=32, verbose_name='Short title (quaternary)'), + ), + migrations.AddField( + model_name='page', + name='short_title_lang5', + field=models.CharField(blank=True, help_text='The short title for this page (in the quinary language), used in the navigation.', max_length=32, verbose_name='Short title (quinary)'), + ), + ] diff --git a/rdmo/questions/migrations/0096_alter_question_value_type.py b/rdmo/questions/migrations/0096_alter_question_value_type.py new file mode 100644 index 0000000000..2a6f2c0ec3 --- /dev/null +++ b/rdmo/questions/migrations/0096_alter_question_value_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-07-18 14:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('questions', '0095_page_short_title'), + ] + + operations = [ + migrations.AlterField( + model_name='question', + name='value_type', + field=models.CharField(choices=[('text', 'Text'), ('url', 'URL'), ('integer', 'Integer'), ('float', 'Float'), ('boolean', 'Boolean'), ('date', 'Date'), ('datetime', 'Datetime'), ('email', 'E-mail'), ('phone', 'Phone'), ('option', 'Option'), ('file', 'File')], help_text='Type of value for this question.', max_length=8, verbose_name='Value type'), + ), + ] diff --git a/rdmo/questions/models/page.py b/rdmo/questions/models/page.py index ad5c8db740..a96e7c7dd6 100644 --- a/rdmo/questions/models/page.py +++ b/rdmo/questions/models/page.py @@ -106,6 +106,31 @@ class Page(Model, TranslationMixin): verbose_name=_('Title (quinary)'), help_text=_('The title for this page (in the quinary language).') ) + short_title_lang1 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (primary)'), + help_text=_('The short title for this page (in the primary language), used in the navigation.') + ) + short_title_lang2 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (secondary)'), + help_text=_('The short title for this page (in the secondary language), used in the navigation.') + ) + short_title_lang3 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (tertiary)'), + help_text=_('The short title for this page (in the tertiary language), used in the navigation.') + ) + short_title_lang4 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (quaternary)'), + help_text=_('The short title for this page (in the quaternary language), used in the navigation.') + ) + short_title_lang5 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (quinary)'), + help_text=_('The short title for this page (in the quinary language), used in the navigation.') + ) help_lang1 = models.TextField( blank=True, verbose_name=_('Help (primary)'), @@ -178,6 +203,10 @@ def save(self, *args, **kwargs): def title(self): return self.trans('title') + @property + def short_title(self): + return self.trans('short_title') + @property def help(self): return self.trans('help') @@ -214,6 +243,7 @@ def to_dict(self): 'id': self.id, 'uri': self.uri, 'title': self.title, + 'short_title': self.short_title, 'is_collection': self.is_collection, 'attribute': self.attribute.uri if self.attribute else None, 'conditions': [condition.uri for condition in self.conditions.all()], diff --git a/rdmo/questions/models/section.py b/rdmo/questions/models/section.py index 7bec2bb00c..c33d530783 100644 --- a/rdmo/questions/models/section.py +++ b/rdmo/questions/models/section.py @@ -89,6 +89,31 @@ class Section(Model, TranslationMixin): verbose_name=_('Title (quinary)'), help_text=_('The title for this section (in the quinary language).') ) + short_title_lang1 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (primary)'), + help_text=_('The short title for this section (in the primary language), used in the navigation.') + ) + short_title_lang2 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (secondary)'), + help_text=_('The short title for this section (in the secondary language), used in the navigation.') + ) + short_title_lang3 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (tertiary)'), + help_text=_('The short title for this section (in the tertiary language), used in the navigation.') + ) + short_title_lang4 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (quaternary)'), + help_text=_('The short title for this section (in the quaternary language), used in the navigation.') + ) + short_title_lang5 = models.CharField( + max_length=32, blank=True, + verbose_name=_('Short title (quinary)'), + help_text=_('The short title for this section (in the quinary language), used in the navigation.') + ) class Meta: ordering = ('uri', ) @@ -106,6 +131,10 @@ def save(self, *args, **kwargs): def title(self): return self.trans('title') + @property + def short_title(self): + return self.trans('short_title') + @cached_property def is_locked(self): return self.locked or any(catalog.is_locked for catalog in self.catalogs.all()) @@ -131,6 +160,7 @@ def to_dict(self): 'id': self.id, 'uri': self.uri, 'title': self.title, + 'short_title': self.short_title, 'elements': elements, 'pages': elements } diff --git a/rdmo/questions/renderers/mixins.py b/rdmo/questions/renderers/mixins.py index 328e14fdef..f9fdb5c9f8 100644 --- a/rdmo/questions/renderers/mixins.py +++ b/rdmo/questions/renderers/mixins.py @@ -14,8 +14,8 @@ def render_catalog(self, xml, catalog): self.render_text_element(xml, 'order', {}, catalog['order']) for lang_code, lang_string, lang_field in get_languages(): - self.render_text_element(xml, 'title', {'lang': lang_code}, catalog['title_%s' % lang_code]) - self.render_text_element(xml, 'help', {'lang': lang_code}, catalog['help_%s' % lang_code]) + self.render_text_element(xml, 'title', {'lang': lang_code}, catalog[f'title_{lang_code}']) + self.render_text_element(xml, 'help', {'lang': lang_code}, catalog[f'help_{lang_code}']) xml.startElement('sections', {}) for catalog_section in catalog['catalog_sections']: @@ -44,7 +44,8 @@ def render_section(self, xml, section): self.render_text_element(xml, 'dc:comment', {}, section['comment']) for lang_code, lang_string, lang_field in get_languages(): - self.render_text_element(xml, 'title', {'lang': lang_code}, section['title_%s' % lang_code]) + self.render_text_element(xml, 'title', {'lang': lang_code}, section[f'title_{lang_code}']) + self.render_text_element(xml, 'short_title', {'lang': lang_code}, section[f'short_title_{lang_code}']) xml.startElement('pages', {}) for section_page in section['section_pages']: @@ -81,11 +82,13 @@ def render_page(self, xml, page): for lang_code, lang_string, lang_field in get_languages(): self.render_text_element(xml, 'title', {'lang': lang_code}, - page['title_%s' % lang_code]) + page[f'title_{lang_code}']) + self.render_text_element(xml, 'short_title', {'lang': lang_code}, + page[f'short_title_{lang_code}']) self.render_text_element(xml, 'help', {'lang': lang_code}, - page['help_%s' % lang_code]) + page[f'help_{lang_code}']) self.render_text_element(xml, 'verbose_name', {'lang': lang_code}, - page['verbose_name_%s' % lang_code]) + page[f'verbose_name_{lang_code}']) xml.startElement('questionsets', {}) for page_questionset in page['page_questionsets']: @@ -147,11 +150,11 @@ def render_questionset(self, xml, questionset): for lang_code, lang_string, lang_field in get_languages(): self.render_text_element(xml, 'title', {'lang': lang_code}, - questionset['title_%s' % lang_code]) + questionset[f'title_{lang_code}']) self.render_text_element(xml, 'help', {'lang': lang_code}, - questionset['help_%s' % lang_code]) + questionset[f'help_{lang_code}']) self.render_text_element(xml, 'verbose_name', {'lang': lang_code}, - questionset['verbose_name_%s' % lang_code]) + questionset[f'verbose_name_{lang_code}']) xml.startElement('questionsets', {}) for questionset_questionset in questionset['questionset_questionsets']: @@ -214,13 +217,13 @@ def render_question(self, xml, question): for lang_code, lang_string, lang_field in get_languages(): self.render_text_element(xml, 'help', {'lang': lang_code}, - question['help_%s' % lang_code]) + question[f'help_{lang_code}']) self.render_text_element(xml, 'text', {'lang': lang_code}, - question['text_%s' % lang_code]) + question[f'text_{lang_code}']) self.render_text_element(xml, 'default_text', {'lang': lang_code}, - question['default_text_%s' % lang_code]) + question[f'default_text_{lang_code}']) self.render_text_element(xml, 'verbose_name', {'lang': lang_code}, - question['verbose_name_%s' % lang_code]) + question[f'verbose_name_{lang_code}']) self.render_text_element(xml, 'default_option', {'dc:uri': question['default_option']}, None) self.render_text_element(xml, 'default_external_id', {}, question['default_external_id']) diff --git a/rdmo/questions/serializers/export.py b/rdmo/questions/serializers/export.py index 1183a84ed4..b659ffdc7c 100644 --- a/rdmo/questions/serializers/export.py +++ b/rdmo/questions/serializers/export.py @@ -159,6 +159,7 @@ class Meta: ) trans_fields = ( 'title', + 'short_title', 'help', 'verbose_name' ) @@ -191,6 +192,7 @@ class Meta: ) trans_fields = ( 'title', + 'short_title' ) diff --git a/rdmo/questions/serializers/v1/catalog.py b/rdmo/questions/serializers/v1/catalog.py index 8d2290c1eb..a79e028bf9 100644 --- a/rdmo/questions/serializers/v1/catalog.py +++ b/rdmo/questions/serializers/v1/catalog.py @@ -3,6 +3,7 @@ from rdmo.core.serializers import ( ElementModelSerializerMixin, ElementWarningSerializerMixin, + MarkdownSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, ThroughModelSerializerMixin, TranslationSerializerMixin, @@ -25,7 +26,10 @@ class Meta: class CatalogSerializer(ThroughModelSerializerMixin, TranslationSerializerMixin, ElementModelSerializerMixin, ElementWarningSerializerMixin, - ReadOnlyObjectPermissionSerializerMixin, serializers.ModelSerializer): + ReadOnlyObjectPermissionSerializerMixin, MarkdownSerializerMixin, + serializers.ModelSerializer): + + markdown_fields = ('title', 'help') model = serializers.SerializerMethodField() uri_path = serializers.CharField(required=True) diff --git a/rdmo/questions/serializers/v1/page.py b/rdmo/questions/serializers/v1/page.py index e10df08f2e..e8c8c0ed17 100644 --- a/rdmo/questions/serializers/v1/page.py +++ b/rdmo/questions/serializers/v1/page.py @@ -3,6 +3,7 @@ from rdmo.core.serializers import ( ElementModelSerializerMixin, ElementWarningSerializerMixin, + MarkdownSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, ThroughModelSerializerMixin, TranslationSerializerMixin, @@ -36,7 +37,10 @@ class Meta: class PageSerializer(ThroughModelSerializerMixin, TranslationSerializerMixin, ElementModelSerializerMixin, ElementWarningSerializerMixin, - ReadOnlyObjectPermissionSerializerMixin, serializers.ModelSerializer): + ReadOnlyObjectPermissionSerializerMixin, MarkdownSerializerMixin, + serializers.ModelSerializer): + + markdown_fields = ('title', 'help') model = serializers.SerializerMethodField() uri_path = serializers.CharField(required=True) @@ -64,6 +68,7 @@ class Meta: 'attribute', 'is_collection', 'title', + 'short_title', 'help', 'verbose_name', 'sections', @@ -78,6 +83,7 @@ class Meta: ) trans_fields = ( 'title', + 'short_title', 'help', 'verbose_name' ) diff --git a/rdmo/questions/serializers/v1/question.py b/rdmo/questions/serializers/v1/question.py index 3235e05fb6..4be6244297 100644 --- a/rdmo/questions/serializers/v1/question.py +++ b/rdmo/questions/serializers/v1/question.py @@ -6,6 +6,7 @@ from rdmo.core.serializers import ( ElementModelSerializerMixin, ElementWarningSerializerMixin, + MarkdownSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, ThroughModelSerializerMixin, TranslationSerializerMixin, @@ -17,7 +18,10 @@ class QuestionSerializer(ThroughModelSerializerMixin, TranslationSerializerMixin, ElementModelSerializerMixin, ElementWarningSerializerMixin, - ReadOnlyObjectPermissionSerializerMixin, serializers.ModelSerializer): + ReadOnlyObjectPermissionSerializerMixin, MarkdownSerializerMixin, + serializers.ModelSerializer): + + markdown_fields = ('text', 'help') model = serializers.SerializerMethodField() uri_path = serializers.CharField(required=True) diff --git a/rdmo/questions/serializers/v1/questionset.py b/rdmo/questions/serializers/v1/questionset.py index c8789f2f73..88e487bab7 100644 --- a/rdmo/questions/serializers/v1/questionset.py +++ b/rdmo/questions/serializers/v1/questionset.py @@ -3,6 +3,7 @@ from rdmo.core.serializers import ( ElementModelSerializerMixin, ElementWarningSerializerMixin, + MarkdownSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, ThroughModelSerializerMixin, TranslationSerializerMixin, @@ -35,7 +36,10 @@ class Meta: class QuestionSetSerializer(ThroughModelSerializerMixin, TranslationSerializerMixin, ElementModelSerializerMixin, ElementWarningSerializerMixin, - ReadOnlyObjectPermissionSerializerMixin, serializers.ModelSerializer): + ReadOnlyObjectPermissionSerializerMixin, MarkdownSerializerMixin, + serializers.ModelSerializer): + + markdown_fields = ('title', 'help') model = serializers.SerializerMethodField() uri_path = serializers.CharField(required=True) diff --git a/rdmo/questions/serializers/v1/section.py b/rdmo/questions/serializers/v1/section.py index fe5187084f..f82f25d780 100644 --- a/rdmo/questions/serializers/v1/section.py +++ b/rdmo/questions/serializers/v1/section.py @@ -3,6 +3,7 @@ from rdmo.core.serializers import ( ElementModelSerializerMixin, ElementWarningSerializerMixin, + MarkdownSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, ThroughModelSerializerMixin, TranslationSerializerMixin, @@ -25,7 +26,10 @@ class Meta: class SectionSerializer(ThroughModelSerializerMixin, TranslationSerializerMixin, ElementModelSerializerMixin, ElementWarningSerializerMixin, - ReadOnlyObjectPermissionSerializerMixin, serializers.ModelSerializer): + ReadOnlyObjectPermissionSerializerMixin, MarkdownSerializerMixin, + serializers.ModelSerializer): + + markdown_fields = ('title', 'help') model = serializers.SerializerMethodField() uri_path = serializers.CharField(required=True) @@ -47,6 +51,7 @@ class Meta: 'comment', 'locked', 'title', + 'short_title', 'catalogs', 'pages', 'editors', @@ -55,6 +60,7 @@ class Meta: ) trans_fields = ( 'title', + 'short_title' ) parent_fields = ( ('catalogs', 'catalog', 'section', 'catalog_sections'), diff --git a/rdmo/questions/tests/test_admin.py b/rdmo/questions/tests/test_admin.py index fb87bde305..2b49c9b235 100644 --- a/rdmo/questions/tests/test_admin.py +++ b/rdmo/questions/tests/test_admin.py @@ -1,41 +1,31 @@ from django.urls import reverse -def test_catalog_search(db, client): - client.login(username='admin', password='admin') - +def test_catalog_search(admin_client): url = reverse('admin:questions_catalog_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 -def test_section_search(db, client): - client.login(username='admin', password='admin') - +def test_section_search(admin_client): url = reverse('admin:questions_section_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 -def test_page_search(db, client): - client.login(username='admin', password='admin') - +def test_page_search(admin_client): url = reverse('admin:questions_page_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 -def test_questionset_search(db, client): - client.login(username='admin', password='admin') - +def test_questionset_search(admin_client): url = reverse('admin:questions_questionset_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 -def test_question_search(db, client): - client.login(username='admin', password='admin') - +def test_question_search(admin_client): url = reverse('admin:questions_question_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 diff --git a/rdmo/questions/tests/test_managers.py b/rdmo/questions/tests/test_managers.py index f6238cacf5..8a16ccab77 100644 --- a/rdmo/questions/tests/test_managers.py +++ b/rdmo/questions/tests/test_managers.py @@ -4,4 +4,4 @@ def test_questions_filter_by_catalog(db): catalog = Catalog.objects.prefetch_elements().first() questions = Question.objects.filter_by_catalog(catalog) - assert questions.count() == 89 + assert questions.count() == 97 diff --git a/rdmo/questions/tests/test_viewset_catalog.py b/rdmo/questions/tests/test_viewset_catalog.py index f6e0d1fc74..e1a4e2cbb7 100644 --- a/rdmo/questions/tests/test_viewset_catalog.py +++ b/rdmo/questions/tests/test_viewset_catalog.py @@ -251,3 +251,20 @@ def test_detail_export(db, client, username, password, export_format): assert root.tag == 'rdmo' for child in root: assert child.tag in ['catalog', 'section', 'page', 'questionset', 'question'] + + +def test_detail_export_full(db, client): + client.login(username='editor', password='editor') + + url = reverse(urlnames['detail_export'], args=[1]) + 'xml/?full=true' + response = client.get(url) + assert response.status_code == status_map['detail']['editor'], response.content + + root = et.fromstring(response.content) + assert root.tag == 'rdmo' + + uris = [child.attrib[r'{http://purl.org/dc/elements/1.1/}uri'] for child in root] + assert 'http://example.com/terms/conditions/options_empty' in uris + assert 'http://example.com/terms/domain/conditions' in uris + assert 'http://example.com/terms/options/one_two_three' in uris + assert 'http://example.com/terms/options/one_two_three/one' in uris diff --git a/rdmo/questions/tests/test_viewset_catalog_multisite.py b/rdmo/questions/tests/test_viewset_catalog_multisite.py index 1a69aff5af..5373671495 100644 --- a/rdmo/questions/tests/test_viewset_catalog_multisite.py +++ b/rdmo/questions/tests/test_viewset_catalog_multisite.py @@ -2,14 +2,18 @@ import pytest +from django.contrib.sites.models import Site from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import Catalog from .test_viewset_catalog import export_formats, urlnames +urlnames['catalog-toggle-site'] = 'v1-questions:catalog-toggle-site' + @pytest.mark.parametrize('username,password', users) def test_list(db, client, username, password): @@ -206,3 +210,37 @@ def test_detail_export(db, client, username, password, export_format): assert root.tag == 'rdmo' for child in root: assert child.tag in ['catalog', 'section', 'page', 'questionset', 'question'] + + +@pytest.mark.parametrize('username,password', users) +@pytest.mark.parametrize('add_or_remove,has_current_site_check', [('add', True), ('remove', False)]) +@pytest.mark.parametrize('locked', [True, False]) +def test_update_catalog_toggle_site(db, client, username, password, add_or_remove, has_current_site_check, locked): + client.login(username=username, password=password) + instances = Catalog.objects.all() + current_site = Site.objects.get_current() + + for instance in instances: + if add_or_remove == 'add': + instance.sites.remove(current_site) + elif add_or_remove == 'remove': + instance.sites.add(current_site) + + # locked state should not affect this toggle + instance.locked = locked + instance.save() + + before_put_has_current_site = instance.sites.filter(id=current_site.id).exists() + + url = reverse(urlnames['catalog-toggle-site'], kwargs={'pk': instance.pk}) + + response = client.put(url, {}, content_type='application/json') + assert response.status_code == get_obj_perms_status_code(instance, username, 'toggle-site'), response.json() + instance.refresh_from_db() + after_put_has_current_site = instance.sites.filter(id=current_site.id).exists() + if response.status_code == 200: + # check if instance now has the current site or not + assert after_put_has_current_site is has_current_site_check + else: + # check that the instance was not updated + assert after_put_has_current_site is before_put_has_current_site diff --git a/rdmo/questions/tests/test_viewset_page.py b/rdmo/questions/tests/test_viewset_page.py index afac8f91ef..e0147bb0b5 100644 --- a/rdmo/questions/tests/test_viewset_page.py +++ b/rdmo/questions/tests/test_viewset_page.py @@ -330,3 +330,20 @@ def test_detail_export(db, client, username, password, export_format): assert root.tag == 'rdmo' for child in root: assert child.tag in ['page', 'questionset', 'question'] + + +def test_detail_export_full(db, client): + client.login(username='editor', password='editor') + + url = reverse(urlnames['detail_export'], args=[71]) + 'xml/?full=true' + response = client.get(url) + assert response.status_code == status_map['detail']['editor'], response.content + + root = et.fromstring(response.content) + assert root.tag == 'rdmo' + + uris = [child.attrib[r'{http://purl.org/dc/elements/1.1/}uri'] for child in root] + assert 'http://example.com/terms/conditions/options_empty' in uris + assert 'http://example.com/terms/domain/conditions' in uris + assert 'http://example.com/terms/options/one_two_three' in uris + assert 'http://example.com/terms/options/one_two_three/one' in uris diff --git a/rdmo/questions/tests/test_viewset_page_multisite.py b/rdmo/questions/tests/test_viewset_page_multisite.py index 98c06785da..6bd56690a8 100644 --- a/rdmo/questions/tests/test_viewset_page_multisite.py +++ b/rdmo/questions/tests/test_viewset_page_multisite.py @@ -5,9 +5,10 @@ from django.db.models import Max from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import Page from .test_viewset_page import export_formats, urlnames diff --git a/rdmo/questions/tests/test_viewset_question.py b/rdmo/questions/tests/test_viewset_question.py index 8456621494..baee8839b5 100644 --- a/rdmo/questions/tests/test_viewset_question.py +++ b/rdmo/questions/tests/test_viewset_question.py @@ -365,3 +365,18 @@ def test_detail_export(db, client, username, password, export_format): assert root.tag == 'rdmo' for child in root: assert child.tag in ['question'] + + +def test_detail_export_full(db, client): + client.login(username='editor', password='editor') + + url = reverse(urlnames['detail_export'], args=[104]) + 'xml/?full=true' + response = client.get(url) + assert response.status_code == status_map['detail']['editor'], response.content + + root = et.fromstring(response.content) + assert root.tag == 'rdmo' + + uris = [child.attrib[r'{http://purl.org/dc/elements/1.1/}uri'] for child in root] + assert 'http://example.com/terms/conditions/set_bool_is_true' in uris + assert 'http://example.com/terms/domain/conditions' in uris diff --git a/rdmo/questions/tests/test_viewset_question_multisite.py b/rdmo/questions/tests/test_viewset_question_multisite.py index 796121c0d9..73264498ed 100644 --- a/rdmo/questions/tests/test_viewset_question_multisite.py +++ b/rdmo/questions/tests/test_viewset_question_multisite.py @@ -5,9 +5,10 @@ from django.db.models import Max from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import Question from .test_viewset_question import export_formats, urlnames diff --git a/rdmo/questions/tests/test_viewset_questionset.py b/rdmo/questions/tests/test_viewset_questionset.py index e3781b7e6d..314e874255 100644 --- a/rdmo/questions/tests/test_viewset_questionset.py +++ b/rdmo/questions/tests/test_viewset_questionset.py @@ -370,3 +370,19 @@ def test_detail_export(db, client, username, password, export_format): assert root.tag == 'rdmo' for child in root: assert child.tag in ['questionset', 'question'] + + +def test_detail_export_full(db, client): + client.login(username='editor', password='editor') + + url = reverse(urlnames['detail_export'], args=[90]) + 'xml/?full=true' + response = client.get(url) + assert response.status_code == status_map['detail']['editor'], response.content + + root = et.fromstring(response.content) + assert root.tag == 'rdmo' + + uris = [child.attrib[r'{http://purl.org/dc/elements/1.1/}uri'] for child in root] + assert 'http://example.com/terms/domain/blocks' in uris + assert 'http://example.com/terms/options/one_two_three' in uris + assert 'http://example.com/terms/options/one_two_three/one' in uris diff --git a/rdmo/questions/tests/test_viewset_questionset_multisite.py b/rdmo/questions/tests/test_viewset_questionset_multisite.py index a45360a800..2a11a36c2c 100644 --- a/rdmo/questions/tests/test_viewset_questionset_multisite.py +++ b/rdmo/questions/tests/test_viewset_questionset_multisite.py @@ -5,9 +5,10 @@ from django.db.models import Max from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import QuestionSet from .test_viewset_questionset import export_formats, urlnames diff --git a/rdmo/questions/tests/test_viewset_section.py b/rdmo/questions/tests/test_viewset_section.py index 3176ef391b..efd64e07da 100644 --- a/rdmo/questions/tests/test_viewset_section.py +++ b/rdmo/questions/tests/test_viewset_section.py @@ -275,3 +275,20 @@ def test_detail_export(db, client, username, password, export_format): assert root.tag == 'rdmo' for child in root: assert child.tag in ['section', 'page', 'questionset', 'question'] + + +def test_detail_export_full(db, client): + client.login(username='editor', password='editor') + + url = reverse(urlnames['detail_export'], args=[5]) + 'xml/?full=true' + response = client.get(url) + assert response.status_code == status_map['detail']['editor'], response.content + + root = et.fromstring(response.content) + assert root.tag == 'rdmo' + + uris = [child.attrib[r'{http://purl.org/dc/elements/1.1/}uri'] for child in root] + assert 'http://example.com/terms/conditions/options_empty' in uris + assert 'http://example.com/terms/domain/conditions' in uris + assert 'http://example.com/terms/options/one_two_three' in uris + assert 'http://example.com/terms/options/one_two_three/one' in uris diff --git a/rdmo/questions/tests/test_viewset_section_multisite.py b/rdmo/questions/tests/test_viewset_section_multisite.py index fcb166fd49..203d783ef4 100644 --- a/rdmo/questions/tests/test_viewset_section_multisite.py +++ b/rdmo/questions/tests/test_viewset_section_multisite.py @@ -5,9 +5,10 @@ from django.db.models import Max from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import Section from .test_viewset_section import export_formats, urlnames diff --git a/rdmo/questions/urls/v1.py b/rdmo/questions/urls/v1.py index d200a4fcc7..c6faacff18 100644 --- a/rdmo/questions/urls/v1.py +++ b/rdmo/questions/urls/v1.py @@ -22,7 +22,6 @@ router.register(r'questions', QuestionViewSet, basename='question') router.register(r'widgettypes', WidgetTypeViewSet, basename='widgettype') router.register(r'valuetypes', ValueTypeViewSet, basename='valuetype') - urlpatterns = [ path('', include(router.urls)), ] diff --git a/rdmo/questions/utils.py b/rdmo/questions/utils.py index 84bed4874f..31d09d53ec 100644 --- a/rdmo/questions/utils.py +++ b/rdmo/questions/utils.py @@ -11,6 +11,14 @@ def get_widget_types(): return [widget.key for widget in widgets.values()] +def get_widget_type_or_default(key=None): + widget_types = get_widget_types() + if key in widget_types: + return key + else: + return widget_types[0] + + def get_widget_type_choices(): widgets = get_plugins('QUESTIONS_WIDGETS') return [(widget.key, widget.label) for widget in widgets.values()] diff --git a/rdmo/questions/viewsets.py b/rdmo/questions/viewsets.py index 33da3dc1de..e5b364642e 100644 --- a/rdmo/questions/viewsets.py +++ b/rdmo/questions/viewsets.py @@ -13,6 +13,7 @@ from rdmo.core.permissions import HasModelPermission, HasObjectPermission from rdmo.core.utils import is_truthy, render_to_format from rdmo.core.views import ChoicesViewSet +from rdmo.management.viewsets import ElementToggleCurrentSiteViewSetMixin from .models import Catalog, Page, Question, QuestionSet, Section from .renderers import CatalogRenderer, PageRenderer, QuestionRenderer, QuestionSetRenderer, SectionRenderer @@ -42,7 +43,7 @@ from .utils import get_widget_type_choices -class CatalogViewSet(ModelViewSet): +class CatalogViewSet(ElementToggleCurrentSiteViewSetMixin, ModelViewSet): permission_classes = (HasModelPermission | HasObjectPermission,) serializer_class = CatalogSerializer diff --git a/rdmo/services/providers.py b/rdmo/services/providers.py index d8262161dd..1ec720a84e 100644 --- a/rdmo/services/providers.py +++ b/rdmo/services/providers.py @@ -47,7 +47,7 @@ def post(self, request, url, json=None, files=None, multipart=None): access_token = self.get_from_session(request, 'access_token') if access_token: # if the access_token is available post to the upstream service - logger.debug('post: %s %s', url, json, files) + logger.debug('post: %s %s %s', url, json, files) if multipart is not None: multipart_encoder = MultipartEncoder(fields=multipart) diff --git a/rdmo/tasks/imports.py b/rdmo/tasks/imports.py index 2f9ef7dca8..df5523da8b 100644 --- a/rdmo/tasks/imports.py +++ b/rdmo/tasks/imports.py @@ -1,59 +1,18 @@ -import logging - -from django.contrib.sites.models import Site - -from rdmo.core.imports import ( - check_permissions, - set_common_fields, - set_foreign_field, - set_lang_field, - set_m2m_instances, - validate_instance, -) +from rdmo.core.import_helpers import ElementImportHelper, ExtraFieldHelper from .models import Task from .validators import TaskLockedValidator, TaskUniqueURIValidator -logger = logging.getLogger(__name__) - - -def import_task(element, save=False, user=None): - try: - task = Task.objects.get(uri=element.get('uri')) - except Task.DoesNotExist: - task = Task() - - set_common_fields(task, element) - - task.order = element.get('order') or 0 - - set_lang_field(task, 'title', element) - set_lang_field(task, 'text', element) - - set_foreign_field(task, 'start_attribute', element) - set_foreign_field(task, 'end_attribute', element) - - task.days_before = element.get('days_before') - task.days_after = element.get('days_after') - - task.available = element.get('available', True) - - validate_instance(task, element, TaskUniqueURIValidator, TaskLockedValidator) - - check_permissions(task, element, user) - - if save and not element.get('errors'): - if task.id: - element['updated'] = True - logger.info('Task %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('Task created with uri %s.', element.get('uri')) - - task.save() - set_m2m_instances(task, 'catalogs', element) - set_m2m_instances(task, 'conditions', element) - task.sites.add(Site.objects.get_current()) - task.editors.add(Site.objects.get_current()) - - return task +import_helper_task = ElementImportHelper( + model=Task, + validators=(TaskLockedValidator, TaskUniqueURIValidator), + lang_fields=('title', 'text'), + foreign_fields=('start_attribute', 'end_attribute'), + extra_fields=( + ExtraFieldHelper(field_name='order'), + ExtraFieldHelper(field_name='days_before'), + ExtraFieldHelper(field_name='days_after'), + ExtraFieldHelper(field_name='available', overwrite_in_element=True), + ), + m2m_instance_fields=('catalogs', 'conditions'), +) diff --git a/rdmo/tasks/renderers/mixins.py b/rdmo/tasks/renderers/mixins.py index adb6d1cc9c..fbcf6fd1ed 100644 --- a/rdmo/tasks/renderers/mixins.py +++ b/rdmo/tasks/renderers/mixins.py @@ -14,8 +14,8 @@ def render_task(self, xml, task): self.render_text_element(xml, 'order', {}, task['order']) for lang_code, lang_string, lang_field in get_languages(): - self.render_text_element(xml, 'title', {'lang': lang_code}, task['title_%s' % lang_code]) - self.render_text_element(xml, 'text', {'lang': lang_code}, task['text_%s' % lang_code]) + self.render_text_element(xml, 'title', {'lang': lang_code}, task[f'title_{lang_code}']) + self.render_text_element(xml, 'text', {'lang': lang_code}, task[f'text_{lang_code}']) self.render_text_element(xml, 'start_attribute', {'dc:uri': task['start_attribute']}, None) self.render_text_element(xml, 'end_attribute', {'dc:uri': task['end_attribute']}, None) @@ -23,13 +23,13 @@ def render_task(self, xml, task): self.render_text_element(xml, 'days_after', {}, task['days_after']) xml.startElement('conditions', {}) - if 'conditions' in task and task['conditions']: + if task.get('conditions'): for condition in task['conditions']: self.render_text_element(xml, 'condition', {'dc:uri': condition['uri']}, None) xml.endElement('conditions') xml.startElement('catalogs', {}) - if 'catalogs' in task and task['catalogs']: + if task.get('catalogs'): for catalog in task['catalogs']: self.render_text_element(xml, 'catalog', {'dc:uri': catalog}, None) xml.endElement('catalogs') diff --git a/rdmo/tasks/serializers/v1.py b/rdmo/tasks/serializers/v1.py index 424c0db487..146322e474 100644 --- a/rdmo/tasks/serializers/v1.py +++ b/rdmo/tasks/serializers/v1.py @@ -3,6 +3,7 @@ from rdmo.core.serializers import ( ElementModelSerializerMixin, ElementWarningSerializerMixin, + MarkdownSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, TranslationSerializerMixin, ) @@ -13,7 +14,9 @@ class TaskSerializer(TranslationSerializerMixin, ElementModelSerializerMixin, ElementWarningSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, - serializers.ModelSerializer): + MarkdownSerializerMixin, serializers.ModelSerializer): + + markdown_fields = ('title', 'text') model = serializers.SerializerMethodField() uri_path = serializers.CharField(required=True) diff --git a/rdmo/tasks/tests/test_admin.py b/rdmo/tasks/tests/test_admin.py index 7bb10a8e11..80705bc7e2 100644 --- a/rdmo/tasks/tests/test_admin.py +++ b/rdmo/tasks/tests/test_admin.py @@ -1,9 +1,7 @@ from django.urls import reverse -def test_task_search(db, client): - client.login(username='admin', password='admin') - +def test_task_search(admin_client): url = reverse('admin:tasks_task_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 diff --git a/rdmo/tasks/tests/test_viewset_task_multisite.py b/rdmo/tasks/tests/test_viewset_task_multisite.py index 2a1ee40f60..fb4e2c9b88 100644 --- a/rdmo/tasks/tests/test_viewset_task_multisite.py +++ b/rdmo/tasks/tests/test_viewset_task_multisite.py @@ -2,14 +2,18 @@ import pytest +from django.contrib.sites.models import Site from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import Task from .test_viewset_task import export_formats, urlnames +urlnames['task-toggle-site'] = 'v1-tasks:task-toggle-site' + @pytest.mark.parametrize('username,password', users) def test_list(db, client, username, password): @@ -132,3 +136,38 @@ def test_detail_export(db, client, username, password, export_format): assert root.tag == 'rdmo' for child in root: assert child.tag in ['task'] + + + +@pytest.mark.parametrize('username,password', users) +@pytest.mark.parametrize('add_or_remove,has_current_site_check', [('add', True), ('remove', False)]) +@pytest.mark.parametrize('locked', [True, False]) +def test_update_task_toggle_site(db, client, username, password, add_or_remove, has_current_site_check, locked): + client.login(username=username, password=password) + instances = Task.objects.all() + current_site = Site.objects.get_current() + + for instance in instances: + if add_or_remove == 'add': + instance.sites.remove(current_site) + elif add_or_remove == 'remove': + instance.sites.add(current_site) + + # locked state should not affect this toggle + instance.locked = locked + instance.save() + + before_has_current_site = instance.sites.filter(id=current_site.id).exists() + + url = reverse(urlnames['task-toggle-site'], kwargs={'pk': instance.pk}) + + response = client.put(url, {}, content_type='application/json') + assert response.status_code == get_obj_perms_status_code(instance, username, 'toggle-site'), response.json() + instance.refresh_from_db() + after_has_current_site = instance.sites.filter(id=current_site.id).exists() + if response.status_code == 200: + # check if instance now has the current site or not + assert after_has_current_site is has_current_site_check + else: + # check that the instance was not updated + assert after_has_current_site is before_has_current_site diff --git a/rdmo/tasks/viewsets.py b/rdmo/tasks/viewsets.py index 57c79e1718..826693d6a6 100644 --- a/rdmo/tasks/viewsets.py +++ b/rdmo/tasks/viewsets.py @@ -10,6 +10,7 @@ from rdmo.core.filters import SearchFilter from rdmo.core.permissions import HasModelPermission, HasObjectPermission from rdmo.core.utils import is_truthy, render_to_format +from rdmo.management.viewsets import ElementToggleCurrentSiteViewSetMixin from .models import Task from .renderers import TaskRenderer @@ -17,7 +18,7 @@ from .serializers.v1 import TaskIndexSerializer, TaskSerializer -class TaskViewSet(ModelViewSet): +class TaskViewSet(ElementToggleCurrentSiteViewSetMixin, ModelViewSet): permission_classes = (HasModelPermission | HasObjectPermission, ) serializer_class = TaskSerializer queryset = Task.objects.select_related('start_attribute', 'end_attribute') \ diff --git a/rdmo/views/imports.py b/rdmo/views/imports.py index ce7a778cc2..89e6be4693 100644 --- a/rdmo/views/imports.py +++ b/rdmo/views/imports.py @@ -1,46 +1,16 @@ -import logging - -from django.contrib.sites.models import Site - -from rdmo.core.imports import check_permissions, set_common_fields, set_lang_field, set_m2m_instances, validate_instance +from rdmo.core.import_helpers import ElementImportHelper, ExtraFieldHelper from .models import View from .validators import ViewLockedValidator, ViewUniqueURIValidator -logger = logging.getLogger(__name__) - - -def import_view(element, save=False, user=None): - try: - view = View.objects.get(uri=element.get('uri')) - except View.DoesNotExist: - view = View() - - set_common_fields(view, element) - - view.order = element.get('order') or 0 - view.template = element.get('template') - - set_lang_field(view, 'title', element) - set_lang_field(view, 'help', element) - - view.available = element.get('available', True) - - validate_instance(view, element, ViewLockedValidator, ViewUniqueURIValidator) - - check_permissions(view, element, user) - - if save and not element.get('errors'): - if view.id: - element['updated'] = True - logger.info('View %s updated.', element.get('uri')) - else: - element['created'] = True - logger.info('View created with uri %s.', element.get('uri')) - - view.save() - set_m2m_instances(view, 'catalogs', element) - view.sites.add(Site.objects.get_current()) - view.editors.add(Site.objects.get_current()) - - return view +import_helper_view = ElementImportHelper( + model=View, + validators=(ViewLockedValidator, ViewUniqueURIValidator), + lang_fields=('help', 'title'), + extra_fields=( + ExtraFieldHelper(field_name='order'), + ExtraFieldHelper(field_name='template'), + ExtraFieldHelper(field_name='available', overwrite_in_element=True), + ), + m2m_instance_fields=('catalogs',), +) diff --git a/rdmo/views/renderers/mixins.py b/rdmo/views/renderers/mixins.py index f6ee263d70..ca709a7e2f 100644 --- a/rdmo/views/renderers/mixins.py +++ b/rdmo/views/renderers/mixins.py @@ -14,11 +14,11 @@ def render_view(self, xml, view): self.render_text_element(xml, 'order', {}, view['order']) for lang_code, lang_string, lang_field in get_languages(): - self.render_text_element(xml, 'title', {'lang': lang_code}, view['title_%s' % lang_code]) - self.render_text_element(xml, 'help', {'lang': lang_code}, view['help_%s' % lang_code]) + self.render_text_element(xml, 'title', {'lang': lang_code}, view[f'title_{lang_code}']) + self.render_text_element(xml, 'help', {'lang': lang_code}, view[f'help_{lang_code}']) xml.startElement('catalogs', {}) - if 'catalogs' in view and view['catalogs']: + if view.get('catalogs'): for catalog in view['catalogs']: self.render_text_element(xml, 'catalog', {'dc:uri': catalog}, None) xml.endElement('catalogs') diff --git a/rdmo/views/serializers/v1.py b/rdmo/views/serializers/v1.py index 6b07fe63de..071fb81f83 100644 --- a/rdmo/views/serializers/v1.py +++ b/rdmo/views/serializers/v1.py @@ -5,6 +5,7 @@ from rdmo.core.serializers import ( ElementModelSerializerMixin, ElementWarningSerializerMixin, + MarkdownSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, TranslationSerializerMixin, ) @@ -15,7 +16,9 @@ class ViewSerializer(TranslationSerializerMixin, ElementModelSerializerMixin, ElementWarningSerializerMixin, ReadOnlyObjectPermissionSerializerMixin, - serializers.ModelSerializer): + MarkdownSerializerMixin, serializers.ModelSerializer): + + markdown_fields = ('title', 'help') model = serializers.SerializerMethodField() uri_path = serializers.CharField(required=True) diff --git a/rdmo/views/templatetags/view_tags.py b/rdmo/views/templatetags/view_tags.py index 44f7b13319..6a3e7d3e58 100644 --- a/rdmo/views/templatetags/view_tags.py +++ b/rdmo/views/templatetags/view_tags.py @@ -111,6 +111,24 @@ def get_set(context, attribute, set_prefix='', project=None): return get_sets(context, attribute, set_prefix=set_prefix, project=project) +@register.simple_tag +def join_values_inline(values=None, separator=',', separator_last=', and', separator_two='and'): + values = values or [] + + if not values: + return '' + + if len(values) == 1: + return values[0]['value_and_unit'] + + if len(values) == 2: + return f"{values[0]['value_and_unit']} {separator_two} {values[1]['value_and_unit']}" + + separators = [separator]*(len(values)-2) + [separator_last] + [''] + text = " ".join(v['value_and_unit'] + s for v,s in zip(values, separators)) + return text + + @register.inclusion_tag('views/tags/value.html', takes_context=True) def render_value(context, attribute, set_prefix='', set_index=0, index=0, project=None): context['value'] = get_value(context, attribute, set_prefix=set_prefix, set_index=set_index, diff --git a/rdmo/views/tests/test_admin.py b/rdmo/views/tests/test_admin.py index 31929d1d6e..cfd5be7228 100644 --- a/rdmo/views/tests/test_admin.py +++ b/rdmo/views/tests/test_admin.py @@ -1,9 +1,7 @@ from django.urls import reverse -def test_view_search(db, client): - client.login(username='admin', password='admin') - +def test_view_search(admin_client): url = reverse('admin:views_view_changelist') + '?q=test' - response = client.get(url) + response = admin_client.get(url) assert response.status_code == 200 diff --git a/rdmo/views/tests/test_viewset_view_multisite.py b/rdmo/views/tests/test_viewset_view_multisite.py index 31be9c2fbe..d14a372c60 100644 --- a/rdmo/views/tests/test_viewset_view_multisite.py +++ b/rdmo/views/tests/test_viewset_view_multisite.py @@ -2,14 +2,17 @@ import pytest +from django.contrib.sites.models import Site from django.urls import reverse -from ...core.tests import get_obj_perms_status_code -from ...core.tests import multisite_status_map as status_map -from ...core.tests import multisite_users as users +from rdmo.core.tests.constants import multisite_status_map as status_map +from rdmo.core.tests.constants import multisite_users as users +from rdmo.core.tests.utils import get_obj_perms_status_code + from ..models import View from .test_viewset_view import export_formats, urlnames +urlnames['view-toggle-site'] = 'v1-views:view-toggle-site' @pytest.mark.parametrize('username,password', users) def test_list(db, client, username, password): @@ -124,3 +127,37 @@ def test_detail_export(db, client, username, password, export_format): assert root.tag == 'rdmo' for child in root: assert child.tag in ['view'] + + +@pytest.mark.parametrize('username,password', users) +@pytest.mark.parametrize('add_or_remove,has_current_site_check', [('add', True), ('remove', False)]) +@pytest.mark.parametrize('locked', [True, False]) +def test_update_view_toggle_site(db, client, username, password, add_or_remove, has_current_site_check, locked): + client.login(username=username, password=password) + instances = View.objects.all() + current_site = Site.objects.get_current() + + for instance in instances: + if add_or_remove == 'add': + instance.sites.remove(current_site) + elif add_or_remove == 'remove': + instance.sites.add(current_site) + + # locked state should not affect this toggle + instance.locked = locked + instance.save() + + before_has_current_site = instance.sites.filter(id=current_site.id).exists() + + url = reverse(urlnames['view-toggle-site'], kwargs={'pk': instance.pk}) + + response = client.put(url, {}, content_type='application/json') + assert response.status_code == get_obj_perms_status_code(instance, username, 'toggle-site'), response.json() + instance.refresh_from_db() + after_has_current_site = instance.sites.filter(id=current_site.id).exists() + if response.status_code == 200: + # check if instance now has the current site or not + assert after_has_current_site is has_current_site_check + else: + # check that the instance was not updated + assert after_has_current_site is before_has_current_site diff --git a/rdmo/views/utils.py b/rdmo/views/utils.py index 76b710de7c..be40dac141 100644 --- a/rdmo/views/utils.py +++ b/rdmo/views/utils.py @@ -79,10 +79,10 @@ def questions(self): def walk(elements): questions = [] for element in elements: - if element.get('elements'): - questions += walk(element.get('elements')) - else: + if element.get('elements') is None: questions.append(element) + else: + questions += walk(element.get('elements')) return questions return walk(self.catalog['elements']) diff --git a/rdmo/views/viewsets.py b/rdmo/views/viewsets.py index 51054a52ad..52b2809571 100644 --- a/rdmo/views/viewsets.py +++ b/rdmo/views/viewsets.py @@ -10,6 +10,7 @@ from rdmo.core.filters import SearchFilter from rdmo.core.permissions import HasModelPermission, HasObjectPermission from rdmo.core.utils import render_to_format +from rdmo.management.viewsets import ElementToggleCurrentSiteViewSetMixin from .models import View from .renderers import ViewRenderer @@ -17,7 +18,7 @@ from .serializers.v1 import ViewIndexSerializer, ViewSerializer -class ViewViewSet(ModelViewSet): +class ViewViewSet(ElementToggleCurrentSiteViewSetMixin, ModelViewSet): permission_classes = (HasModelPermission | HasObjectPermission, ) serializer_class = ViewSerializer queryset = View.objects.prefetch_related('catalogs', 'sites', 'editors', 'groups') \ diff --git a/testing/config/settings/base.py b/testing/config/settings/base.py index 2d8720fcf3..302104415e 100644 --- a/testing/config/settings/base.py +++ b/testing/config/settings/base.py @@ -63,6 +63,8 @@ ACCOUNT_SIGNUP = True SOCIALACCOUNT = False +PROJECT_TABLE_PAGE_SIZE = 5 + PROJECT_SEND_ISSUE = True PROJECT_SEND_INVITE = True @@ -81,3 +83,11 @@ PROJECT_ISSUE_PROVIDERS = [ ('simple', _('Simple provider'), 'rdmo.projects.providers.SimpleIssueProvider') ] + +PROJECT_IMPORTS += [ + ('url', _('from URL'), 'rdmo.projects.imports.URLImport'), +] + +PROJECT_IMPORTS_LIST = ['url'] + +PROJECT_VALUES_VALIDATION = True diff --git a/testing/export/project.html b/testing/export/project.html index b961e65aa4..1da6e11571 100644 --- a/testing/export/project.html +++ b/testing/export/project.html @@ -6,50 +6,50 @@ <h1>Answers for <em>Test</em></h1> <h2>Single questions</h2> <h3>Text</h3> - <p><strong>Text?</strong></p> + <p><strong class="question-label">Text?</strong></p> <p> <span>Lorem ipsum dolor sit amet</span> </p> <h3>Textarea</h3> - <p><strong>Textarea?</strong></p> + <p><strong class="question-label">Textarea?</strong></p> <p> <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> </p> <h3>Yes or no</h3> - <p><strong>Yes or no?</strong></p> + <p><strong class="question-label">Yes or no?</strong></p> <p> <span>Yes</span> </p> <h3>Radio buttons</h3> - <p><strong>Radio buttons?</strong></p> + <p><strong class="question-label">Radio buttons?</strong></p> <p> <span>Text: Lorem ipsum</span> </p> <h3>Select drop-down</h3> - <p><strong>Select drop-down?</strong></p> + <p><strong class="question-label">Select drop-down?</strong></p> <p> <span>One</span> </p> <h3>Autocomplete</h3> - <p><strong>Autocomplete?</strong></p> + <p><strong class="question-label">Autocomplete?</strong></p> <h3>Range slider</h3> - <p><strong>Range slider?</strong></p> + <p><strong class="question-label">Range slider?</strong></p> <p> <span>37</span> </p> <h3>File</h3> - <p><strong>File?</strong></p> + <p><strong class="question-label">File?</strong></p> <p> <img src="rdmo-logo.svg" alt="rdmo-logo.svg" /> </p> <h3>Datetime</h3> - <p><strong>Date picker?</strong></p> + <p><strong class="question-label">Date picker?</strong></p> <p> <span>Jan. 1, 2018</span> </p> <h2>Collections</h2> <h3>Text</h3> - <p><strong>Text?</strong></p> + <p><strong class="question-label">Text?</strong></p> <ul> <li> <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr</span> @@ -59,7 +59,7 @@ <h3>Text</h3> </li> </ul> <h3>Textarea</h3> - <p><strong>Textarea?</strong></p> + <p><strong class="question-label">Textarea?</strong></p> <ul> <li> <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> @@ -69,7 +69,7 @@ <h3>Textarea</h3> </li> </ul> <h3>Yes or no</h3> - <p><strong>Yes or no?</strong></p> + <p><strong class="question-label">Yes or no?</strong></p> <ul> <li> <span>Yes</span> @@ -82,7 +82,7 @@ <h3>Yes or no</h3> </li> </ul> <h3>Radio buttons</h3> - <p><strong>Radio buttons?</strong></p> + <p><strong class="question-label">Radio buttons?</strong></p> <ul> <li> <span>One</span> @@ -95,7 +95,7 @@ <h3>Radio buttons</h3> </li> </ul> <h3>Select drop-down</h3> - <p><strong>Select drop-down?</strong></p> + <p><strong class="question-label">Select drop-down?</strong></p> <ul> <li> <span>One</span> @@ -108,9 +108,9 @@ <h3>Select drop-down</h3> </li> </ul> <h3>Autocomplete</h3> - <p><strong>Autocomplete?</strong></p> + <p><strong class="question-label">Autocomplete?</strong></p> <h3>Range slider</h3> - <p><strong>Range slider?</strong></p> + <p><strong class="question-label">Range slider?</strong></p> <ul> <li> <span>0</span> @@ -123,7 +123,7 @@ <h3>Range slider</h3> </li> </ul> <h3>Date picker</h3> - <p><strong>Date picker?</strong></p> + <p><strong class="question-label">Date picker?</strong></p> <ul> <li> <span>April 1, 2017</span> @@ -136,7 +136,7 @@ <h3>Date picker</h3> </li> </ul> <h3>File</h3> - <p><strong>File?</strong></p> + <p><strong class="question-label">File?</strong></p> <ul> <li> <a href="test.txt">test.txt</a> @@ -146,7 +146,7 @@ <h3>File</h3> </li> </ul> <h3>Checkbox</h3> - <p><strong>Checkbox?</strong></p> + <p><strong class="question-label">Checkbox?</strong></p> <ul> <li> <span>One</span> @@ -157,41 +157,41 @@ <h3>Checkbox</h3> </ul> <h2>Sets</h2> <h3>Individual sets I</h3> - <p><strong>Text?</strong></p> + <p><strong class="question-label">Text?</strong></p> <p> <span>Lorem ipsum dolor sit amet</span> </p> - <p><strong>Textarea?</strong></p> + <p><strong class="question-label">Textarea?</strong></p> <p> <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> </p> - <p><strong>Yes or no?</strong></p> + <p><strong class="question-label">Yes or no?</strong></p> <p> <span>Yes</span> </p> - <p><strong>Radio buttons?</strong></p> + <p><strong class="question-label">Radio buttons?</strong></p> <p> <span>Text: Lorem ipsum</span> </p> - <p><strong>Select drop-down?</strong></p> + <p><strong class="question-label">Select drop-down?</strong></p> <p> <span>One</span> </p> - <p><strong>Autocomplete?</strong></p> - <p><strong>Range slider?</strong></p> + <p><strong class="question-label">Autocomplete?</strong></p> + <p><strong class="question-label">Range slider?</strong></p> <p> <span>37</span> </p> - <p><strong>Date picker?</strong></p> + <p><strong class="question-label">Date picker?</strong></p> <p> <span>Jan. 1, 2018</span> </p> - <p><strong>File?</strong></p> + <p><strong class="question-label">File?</strong></p> <p> <img src="rdmo-logo.svg" alt="rdmo-logo.svg" /> </p> <h3>Individual sets II</h3> - <p><strong>Text?</strong></p> + <p><strong class="question-label">Text?</strong></p> <ul> <li> <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr</span> @@ -200,7 +200,7 @@ <h3>Individual sets II</h3> <span>sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua</span> </li> </ul> - <p><strong>Textarea?</strong></p> + <p><strong class="question-label">Textarea?</strong></p> <ul> <li> <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> @@ -209,7 +209,7 @@ <h3>Individual sets II</h3> <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> </li> </ul> - <p><strong>Yes or no?</strong></p> + <p><strong class="question-label">Yes or no?</strong></p> <ul> <li> <span>Yes</span> @@ -221,7 +221,7 @@ <h3>Individual sets II</h3> <span>Yes</span> </li> </ul> - <p><strong>Radio buttons?</strong></p> + <p><strong class="question-label">Radio buttons?</strong></p> <ul> <li> <span>One</span> @@ -233,7 +233,7 @@ <h3>Individual sets II</h3> <span>Three</span> </li> </ul> - <p><strong>Select drop-down?</strong></p> + <p><strong class="question-label">Select drop-down?</strong></p> <ul> <li> <span>One</span> @@ -245,8 +245,8 @@ <h3>Individual sets II</h3> <span>Three</span> </li> </ul> - <p><strong>Autocomplete?</strong></p> - <p><strong>Range slider?</strong></p> + <p><strong class="question-label">Autocomplete?</strong></p> + <p><strong class="question-label">Range slider?</strong></p> <ul> <li> <span>0</span> @@ -258,7 +258,7 @@ <h3>Individual sets II</h3> <span>100</span> </li> </ul> - <p><strong>Date picker?</strong></p> + <p><strong class="question-label">Date picker?</strong></p> <ul> <li> <span>April 1, 2017</span> @@ -270,7 +270,7 @@ <h3>Individual sets II</h3> <span>April 3, 2017</span> </li> </ul> - <p><strong>File?</strong></p> + <p><strong class="question-label">File?</strong></p> <ul> <li> <a href="test.txt">test.txt</a> @@ -279,7 +279,7 @@ <h3>Individual sets II</h3> <img src="favicon.png" alt="favicon.png" /> </li> </ul> - <p><strong>Checkbox?</strong></p> + <p><strong class="question-label">Checkbox?</strong></p> <ul> <li> <span>One</span> @@ -289,7 +289,7 @@ <h3>Individual sets II</h3> </li> </ul> <h3>Set collections I</h3> - <p><strong>Text?</strong></p> + <p><strong class="question-label">Text?</strong></p> <p> <em>Set "First":</em>  <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr</span> @@ -298,7 +298,7 @@ <h3>Set collections I</h3> <em>Set "Second":</em>  <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr</span> </p> - <p><strong>Textarea?</strong></p> + <p><strong class="question-label">Textarea?</strong></p> <p> <em>Set "First":</em>  <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> @@ -307,7 +307,7 @@ <h3>Set collections I</h3> <em>Set "Second":</em>  <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> </p> - <p><strong>Yes or no?</strong></p> + <p><strong class="question-label">Yes or no?</strong></p> <p> <em>Set "First":</em>  <span>Yes</span> @@ -316,7 +316,7 @@ <h3>Set collections I</h3> <em>Set "Second":</em>  <span>No</span> </p> - <p><strong>Radio buttons?</strong></p> + <p><strong class="question-label">Radio buttons?</strong></p> <p> <em>Set "First":</em>  <span>One</span> @@ -325,7 +325,7 @@ <h3>Set collections I</h3> <em>Set "Second":</em>  <span>Two</span> </p> - <p><strong>Select drop-down?</strong></p> + <p><strong class="question-label">Select drop-down?</strong></p> <p> <em>Set "First":</em>  <span>One</span> @@ -334,8 +334,8 @@ <h3>Set collections I</h3> <em>Set "Second":</em>  <span>Two</span> </p> - <p><strong>Autocomplete?</strong></p> - <p><strong>Range slider?</strong></p> + <p><strong class="question-label">Autocomplete?</strong></p> + <p><strong class="question-label">Range slider?</strong></p> <p> <em>Set "First":</em>  <span>1</span> @@ -344,7 +344,7 @@ <h3>Set collections I</h3> <em>Set "Second":</em>  <span>2</span> </p> - <p><strong>Date picker?</strong></p> + <p><strong class="question-label">Date picker?</strong></p> <p> <em>Set "First":</em>  <span>Jan. 7, 2018</span> @@ -353,9 +353,9 @@ <h3>Set collections I</h3> <em>Set "Second":</em>  <span>Feb. 7, 2018</span> </p> - <p><strong>File?</strong></p> + <p><strong class="question-label">File?</strong></p> <h3>Set collections II</h3> - <p><strong>Text?</strong></p> + <p><strong class="question-label">Text?</strong></p> <p> <em>Set "First":</em>  <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr</span> @@ -364,7 +364,7 @@ <h3>Set collections II</h3> <em>Set "Second":</em>  <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr</span> </p> - <p><strong>Textarea?</strong></p> + <p><strong class="question-label">Textarea?</strong></p> <p> <em>Set "First":</em>  <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> @@ -373,7 +373,7 @@ <h3>Set collections II</h3> <em>Set "Second":</em>  <span>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet.</span> </p> - <p><strong>Yes or no?</strong></p> + <p><strong class="question-label">Yes or no?</strong></p> <p> <em>Set "First":</em> </p> @@ -402,7 +402,7 @@ <h3>Set collections II</h3> <span>No</span> </li> </ul> - <p><strong>Radio buttons?</strong></p> + <p><strong class="question-label">Radio buttons?</strong></p> <p> <em>Set "First":</em> </p> @@ -431,7 +431,7 @@ <h3>Set collections II</h3> <span>One</span> </li> </ul> - <p><strong>Select drop-down?</strong></p> + <p><strong class="question-label">Select drop-down?</strong></p> <p> <em>Set "First":</em> </p> @@ -447,8 +447,8 @@ <h3>Set collections II</h3> <em>Set "Second":</em>  <span>Three</span> </p> - <p><strong>Autocomplete?</strong></p> - <p><strong>Range slider?</strong></p> + <p><strong class="question-label">Autocomplete?</strong></p> + <p><strong class="question-label">Range slider?</strong></p> <p> <em>Set "First":</em> </p> @@ -464,7 +464,7 @@ <h3>Set collections II</h3> <em>Set "Second":</em>  <span>86</span> </p> - <p><strong>Date picker?</strong></p> + <p><strong class="question-label">Date picker?</strong></p> <p> <em>Set "First":</em> </p> @@ -487,8 +487,8 @@ <h3>Set collections II</h3> <span>Nov. 7, 2018</span> </li> </ul> - <p><strong>File?</strong></p> - <p><strong>Checkbox?</strong></p> + <p><strong class="question-label">File?</strong></p> + <p><strong class="question-label">Checkbox?</strong></p> <p> <em>Set "First":</em> </p> @@ -506,82 +506,82 @@ <h3>Set collections II</h3> </p> <h2>Conditions</h2> <h3>Input</h3> - <p><strong>Text</strong></p> + <p><strong class="question-label">Text</strong></p> <p> <span>test</span> </p> - <p><strong>Option</strong></p> + <p><strong class="question-label">Option</strong></p> <p> <span>One</span> </p> <h3>Text I</h3> - <p><strong>text_contains?</strong></p> + <p><strong class="question-label">text_contains?</strong></p> <p> <span>test</span> </p> <h3>Text II</h3> - <p><strong>text empty?</strong></p> + <p><strong class="question-label">text empty?</strong></p> <h3>Text III</h3> - <p><strong>text_equal?</strong></p> + <p><strong class="question-label">text_equal?</strong></p> <p> <span>test</span> </p> <h3>Text IV</h3> - <p><strong>text_greater_than?</strong></p> + <p><strong class="question-label">text_greater_than?</strong></p> <h3>Text V</h3> - <p><strong>text_greater_than_equal?</strong></p> + <p><strong class="question-label">text_greater_than_equal?</strong></p> <h3>Text VI</h3> - <p><strong>text_lesser_than?</strong></p> + <p><strong class="question-label">text_lesser_than?</strong></p> <h3>Text VII</h3> - <p><strong>text_lesser_than_equal?</strong></p> + <p><strong class="question-label">text_lesser_than_equal?</strong></p> <h3>Text VIII</h3> - <p><strong>text_not_empty?</strong></p> + <p><strong class="question-label">text_not_empty?</strong></p> <p> <span>test</span> </p> <h3>Text IX</h3> - <p><strong>text_not_equal?</strong></p> + <p><strong class="question-label">text_not_equal?</strong></p> <h3>Options I</h3> - <p><strong>option_empty?</strong></p> + <p><strong class="question-label">option_empty?</strong></p> <h3>Options II</h3> - <p><strong>option_equal?</strong></p> + <p><strong class="question-label">option_equal?</strong></p> <p> <span>One</span> </p> <h3>Options III</h3> - <p><strong>option_not_empty?</strong></p> + <p><strong class="question-label">option_not_empty?</strong></p> <p> <span>One</span> </p> <h3>Options IV</h3> - <p><strong>option_not_equal?</strong></p> + <p><strong class="question-label">option_not_equal?</strong></p> <h3>Set I</h3> - <p><strong>Yes/No</strong></p> - <p><strong>Text</strong></p> + <p><strong class="question-label">Yes/No</strong></p> + <p><strong class="question-label">Text</strong></p> <h3>Set II</h3> - <p><strong>Yes/No</strong></p> - <p><strong>Text</strong></p> + <p><strong class="question-label">Yes/No</strong></p> + <p><strong class="question-label">Text</strong></p> <h3>Optionset</h3> - <p><strong>Yes/No</strong></p> - <p><strong>Optionset</strong></p> + <p><strong class="question-label">Yes/No</strong></p> + <p><strong class="question-label">Optionset</strong></p> <h3>Text Set</h3> - <p><strong>Yes/No</strong></p> + <p><strong class="question-label">Yes/No</strong></p> <h2>Options</h2> <h3>Order</h3> - <p><strong>order?</strong></p> + <p><strong class="question-label">order?</strong></p> <h3>Radio</h3> - <p><strong>radio?</strong></p> + <p><strong class="question-label">radio?</strong></p> <h3>Select</h3> - <p><strong>select?</strong></p> + <p><strong class="question-label">select?</strong></p> <h3>Autocomplete</h3> - <p><strong>autocomplete?</strong></p> + <p><strong class="question-label">autocomplete?</strong></p> <h3>Checkboxes</h3> - <p><strong>checkboxes?</strong></p> + <p><strong class="question-label">checkboxes?</strong></p> <h3>Combined</h3> - <p><strong>combined?</strong></p> - <h2>Blocks</h2> - <h3>Set</h3> - <p><strong>A?</strong></p> + <p><strong class="question-label">combined?</strong></p> + <h2>Blocks of questionsets and questions</h2> + <h3>A set of questionsets and questions</h3> + <p><strong class="question-label">A?</strong></p> <p> <em>Set "First", Block #1:</em>  <span>a0</span> @@ -590,7 +590,7 @@ <h3>Set</h3> <em>Set "First", Block #2:</em>  <span>a1</span> </p> - <p><strong>B?</strong></p> + <p><strong class="question-label">B?</strong></p> <p> <em>Set "First", Block #1:</em>  <span>b1</span> @@ -599,7 +599,7 @@ <h3>Set</h3> <em>Set "First", Block #2:</em>  <span>b1</span> </p> - <p><strong>C?</strong></p> + <p><strong class="question-label">C?</strong></p> <p> <em>Set "First", Block #1:</em> </p> @@ -622,7 +622,7 @@ <h3>Set</h3> <span>c11</span> </li> </ul> - <p><strong>Y?</strong></p> + <p><strong class="question-label">Y?</strong></p> <p> <em>Set "Second", Block #3, Set #1:</em>  <span>One</span> @@ -635,5 +635,22 @@ <h3>Set</h3> <em>Set "Second", Block #3, Set #3:</em>  <span>Three</span> </p> + <h2>Validation</h2> + <h3>URL</h3> + <p><strong class="question-label">URL</strong></p> + <h3>Integer</h3> + <p><strong class="question-label">Integer</strong></p> + <h3>Float</h3> + <p><strong class="question-label">Float</strong></p> + <h3>Bool</h3> + <p><strong class="question-label">Bool</strong></p> + <h3>Date</h3> + <p><strong class="question-label">Date</strong></p> + <h3>Datetime</h3> + <p><strong class="question-label">Datetime</strong></p> + <h3>Email</h3> + <p><strong class="question-label">Email</strong></p> + <h3>Phone</h3> + <p><strong class="question-label">Phone</strong></p> </body> -</html> \ No newline at end of file +</html> diff --git a/testing/fixtures/domain.json b/testing/fixtures/domain.json index 7add8d157d..b10abe2b02 100644 --- a/testing/fixtures/domain.json +++ b/testing/fixtures/domain.json @@ -1586,5 +1586,185 @@ 3 ] } + }, + { + "model": "domain.attribute", + "pk": 124, + "fields": { + "uri": "http://example.com/terms/domain/validation", + "uri_prefix": "http://example.com/terms", + "key": "validation", + "path": "validation", + "comment": "", + "locked": false, + "parent": null, + "lft": 1, + "rght": 16, + "tree_id": 9, + "level": 0, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 125, + "fields": { + "uri": "http://example.com/terms/domain/validation/url", + "uri_prefix": "http://example.com/terms", + "key": "url", + "path": "validation/url", + "comment": "", + "locked": false, + "parent": 124, + "lft": 2, + "rght": 3, + "tree_id": 9, + "level": 1, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 126, + "fields": { + "uri": "http://example.com/terms/domain/validation/float", + "uri_prefix": "http://example.com/terms", + "key": "float", + "path": "validation/float", + "comment": "", + "locked": false, + "parent": 124, + "lft": 4, + "rght": 5, + "tree_id": 9, + "level": 1, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 127, + "fields": { + "uri": "http://example.com/terms/domain/validation/integer", + "uri_prefix": "http://example.com/terms", + "key": "integer", + "path": "validation/integer", + "comment": "", + "locked": false, + "parent": 124, + "lft": 6, + "rght": 7, + "tree_id": 9, + "level": 1, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 128, + "fields": { + "uri": "http://example.com/terms/domain/bool", + "uri_prefix": "http://example.com/terms", + "key": "bool", + "path": "bool", + "comment": "", + "locked": false, + "parent": null, + "lft": 1, + "rght": 2, + "tree_id": 10, + "level": 0, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 129, + "fields": { + "uri": "http://example.com/terms/domain/validation/date", + "uri_prefix": "http://example.com/terms", + "key": "date", + "path": "validation/date", + "comment": "", + "locked": false, + "parent": 124, + "lft": 8, + "rght": 9, + "tree_id": 9, + "level": 1, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 130, + "fields": { + "uri": "http://example.com/terms/domain/validation/datetime", + "uri_prefix": "http://example.com/terms", + "key": "datetime", + "path": "validation/datetime", + "comment": "", + "locked": false, + "parent": 124, + "lft": 8, + "rght": 9, + "tree_id": 9, + "level": 1, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 131, + "fields": { + "uri": "http://example.com/terms/domain/validation/email", + "uri_prefix": "http://example.com/terms", + "key": "email", + "path": "validation/email", + "comment": "", + "locked": false, + "parent": 124, + "lft": 10, + "rght": 11, + "tree_id": 9, + "level": 1, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 132, + "fields": { + "uri": "http://example.com/terms/domain/validation/phone", + "uri_prefix": "http://example.com/terms", + "key": "phone", + "path": "validation/phone", + "comment": "", + "locked": false, + "parent": 124, + "lft": 12, + "rght": 13, + "tree_id": 9, + "level": 1, + "editors": [] + } + }, + { + "model": "domain.attribute", + "pk": 133, + "fields": { + "uri": "http://example.com/terms/domain/validation/bool", + "uri_prefix": "http://example.com/terms", + "key": "bool", + "path": "validation/bool", + "comment": "", + "locked": false, + "parent": 124, + "lft": 14, + "rght": 15, + "tree_id": 9, + "level": 1, + "editors": [] + } } ] diff --git a/testing/fixtures/questions.json b/testing/fixtures/questions.json index 4db230d2d9..6e1794be0b 100644 --- a/testing/fixtures/questions.json +++ b/testing/fixtures/questions.json @@ -195,6 +195,15 @@ "order": 1 } }, + { + "model": "questions.catalogsection", + "pk": 9, + "fields": { + "catalog": 1, + "section": 11, + "order": 7 + } + }, { "model": "questions.page", "pk": 1, @@ -213,6 +222,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -245,6 +259,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -277,6 +296,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -309,6 +333,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -341,6 +370,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -373,6 +407,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -405,6 +444,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -437,6 +481,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -469,6 +518,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -501,6 +555,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -533,6 +592,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -565,6 +629,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -597,6 +666,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -629,6 +703,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -661,6 +740,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -693,6 +777,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -725,6 +814,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -757,6 +851,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -789,6 +888,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -821,6 +925,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -855,6 +964,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -889,6 +1003,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -923,6 +1042,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -957,6 +1081,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -991,6 +1120,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1025,6 +1159,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1059,6 +1198,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1093,6 +1237,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1127,6 +1276,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1161,6 +1315,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1195,6 +1354,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1229,6 +1393,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1263,6 +1432,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1295,6 +1469,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1327,6 +1506,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1359,6 +1543,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1391,6 +1580,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1423,6 +1617,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1455,6 +1654,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1487,6 +1691,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1519,6 +1728,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1551,6 +1765,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1570,7 +1789,7 @@ "pk": 87, "fields": { "created": "2021-05-28T14:37:33.097Z", - "updated": "2021-06-02T14:26:33.898Z", + "updated": "2024-06-20T08:21:57.069Z", "uri": "http://example.com/terms/questions/catalog/blocks/set", "uri_prefix": "http://example.com/terms", "uri_path": "catalog/blocks/set", @@ -1578,11 +1797,16 @@ "locked": false, "attribute": 79, "is_collection": true, - "title_lang1": "Set", - "title_lang2": "Set", + "title_lang1": "A set of questionsets and questions", + "title_lang2": "Ein Set von Fragensets und Fragen", "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "Set", + "short_title_lang2": "Set", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1615,6 +1839,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1647,6 +1876,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1679,6 +1913,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1711,6 +1950,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1743,6 +1987,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1777,6 +2026,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1811,6 +2065,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "help_lang1": "", "help_lang2": "", "help_lang3": "", @@ -1827,6 +2086,302 @@ "conditions": [] } }, + { + "model": "questions.page", + "pk": 99, + "fields": { + "created": "2024-06-21T08:08:40.163Z", + "updated": "2024-06-21T08:13:29.009Z", + "uri": "http://example.com/terms/questions/validation/url", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/url", + "comment": "", + "locked": false, + "attribute": null, + "is_collection": false, + "title_lang1": "URL", + "title_lang2": "URL", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "editors": [], + "conditions": [] + } + }, + { + "model": "questions.page", + "pk": 100, + "fields": { + "created": "2024-06-21T08:09:02.853Z", + "updated": "2024-06-21T08:11:14.679Z", + "uri": "http://example.com/terms/questions/validation/integer", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/integer", + "comment": "", + "locked": false, + "attribute": null, + "is_collection": false, + "title_lang1": "Integer", + "title_lang2": "Ganzzahl", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "editors": [], + "conditions": [] + } + }, + { + "model": "questions.page", + "pk": 101, + "fields": { + "created": "2024-06-21T08:09:22.435Z", + "updated": "2024-06-21T08:11:05.825Z", + "uri": "http://example.com/terms/questions/validation/float", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/float", + "comment": "", + "locked": false, + "attribute": null, + "is_collection": false, + "title_lang1": "Float", + "title_lang2": "Gleitkommazahl", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "editors": [], + "conditions": [] + } + }, + { + "model": "questions.page", + "pk": 103, + "fields": { + "created": "2024-06-21T08:10:56.348Z", + "updated": "2024-06-21T08:10:56.348Z", + "uri": "http://example.com/terms/questions/validation/bool", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/bool", + "comment": "", + "locked": false, + "attribute": null, + "is_collection": false, + "title_lang1": "Bool", + "title_lang2": "Boolsche Variable", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "editors": [], + "conditions": [] + } + }, + { + "model": "questions.page", + "pk": 104, + "fields": { + "created": "2024-06-21T08:11:55.340Z", + "updated": "2024-06-21T08:11:55.340Z", + "uri": "http://example.com/terms/questions/validation/datetime", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/datetime", + "comment": "", + "locked": false, + "attribute": null, + "is_collection": false, + "title_lang1": "Datetime", + "title_lang2": "Datum und Zeit", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "editors": [], + "conditions": [] + } + }, + { + "model": "questions.page", + "pk": 105, + "fields": { + "created": "2024-06-21T08:12:20.763Z", + "updated": "2024-06-21T08:12:20.763Z", + "uri": "http://example.com/terms/questions/validation/email", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/email", + "comment": "", + "locked": false, + "attribute": null, + "is_collection": false, + "title_lang1": "Email", + "title_lang2": "Email", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "editors": [], + "conditions": [] + } + }, + { + "model": "questions.page", + "pk": 106, + "fields": { + "created": "2024-06-21T08:12:41.946Z", + "updated": "2024-06-21T08:13:29.008Z", + "uri": "http://example.com/terms/questions/validation/phone", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/phone", + "comment": "", + "locked": false, + "attribute": null, + "is_collection": false, + "title_lang1": "Phone", + "title_lang2": "Telefon", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "editors": [], + "conditions": [] + } + }, + { + "model": "questions.page", + "pk": 107, + "fields": { + "created": "2024-07-18T14:42:58.466Z", + "updated": "2024-07-18T14:46:44.026Z", + "uri": "http://example.com/terms/questions/validation/date", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/date", + "comment": "", + "locked": false, + "attribute": null, + "is_collection": false, + "title_lang1": "Date", + "title_lang2": "Datum", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "editors": [], + "conditions": [] + } + }, { "model": "questions.pagequestion", "pk": 1, @@ -2584,52 +3139,124 @@ } }, { - "model": "questions.pagequestionset", - "pk": 1, + "model": "questions.pagequestion", + "pk": 86, "fields": { - "page": 87, - "questionset": 90, + "page": 106, + "question": 111, "order": 0 } }, { - "model": "questions.pagequestionset", - "pk": 2, + "model": "questions.pagequestion", + "pk": 87, "fields": { - "page": 93, - "questionset": 94, + "page": 99, + "question": 112, "order": 1 } }, { - "model": "questions.pagequestionset", - "pk": 3, + "model": "questions.pagequestion", + "pk": 88, "fields": { - "page": 97, - "questionset": 95, - "order": 0 + "page": 105, + "question": 113, + "order": 1 } }, { - "model": "questions.pagequestionset", - "pk": 4, + "model": "questions.pagequestion", + "pk": 89, "fields": { - "page": 98, - "questionset": 96, - "order": 0 + "page": 104, + "question": 114, + "order": 1 } }, { - "model": "questions.question", - "pk": 1, + "model": "questions.pagequestion", + "pk": 90, "fields": { - "created": "2016-12-15T13:28:15.582Z", - "updated": "2021-04-20T12:11:08.213Z", - "uri": "http://example.com/terms/questions/catalog/individual/text/text", - "uri_prefix": "http://example.com/terms", - "uri_path": "catalog/individual/text/text", - "comment": "", - "locked": false, + "page": 103, + "question": 115, + "order": 1 + } + }, + { + "model": "questions.pagequestion", + "pk": 91, + "fields": { + "page": 101, + "question": 116, + "order": 1 + } + }, + { + "model": "questions.pagequestion", + "pk": 92, + "fields": { + "page": 100, + "question": 117, + "order": 1 + } + }, + { + "model": "questions.pagequestion", + "pk": 94, + "fields": { + "page": 107, + "question": 118, + "order": 1 + } + }, + { + "model": "questions.pagequestionset", + "pk": 1, + "fields": { + "page": 87, + "questionset": 90, + "order": 0 + } + }, + { + "model": "questions.pagequestionset", + "pk": 2, + "fields": { + "page": 93, + "questionset": 94, + "order": 1 + } + }, + { + "model": "questions.pagequestionset", + "pk": 3, + "fields": { + "page": 97, + "questionset": 95, + "order": 0 + } + }, + { + "model": "questions.pagequestionset", + "pk": 4, + "fields": { + "page": 98, + "questionset": 96, + "order": 0 + } + }, + { + "model": "questions.question", + "pk": 1, + "fields": { + "created": "2016-12-15T13:28:15.582Z", + "updated": "2021-04-20T12:11:08.213Z", + "uri": "http://example.com/terms/questions/catalog/individual/text/text", + "uri_prefix": "http://example.com/terms", + "uri_path": "catalog/individual/text/text", + "comment": "", + "locked": false, "attribute": 3, "is_collection": false, "is_optional": false, @@ -7070,6 +7697,390 @@ "conditions": [] } }, + { + "model": "questions.question", + "pk": 111, + "fields": { + "created": "2024-06-21T08:13:24.116Z", + "updated": "2024-06-21T08:21:36.629Z", + "uri": "http://example.com/terms/questions/validation/phone/phone", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/phone/phone", + "comment": "", + "locked": false, + "attribute": 131, + "is_collection": false, + "is_optional": false, + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "text_lang1": "Phone", + "text_lang2": "Telefon", + "text_lang3": "", + "text_lang4": "", + "text_lang5": "", + "default_text_lang1": "", + "default_text_lang2": "", + "default_text_lang3": "", + "default_text_lang4": "", + "default_text_lang5": "", + "default_option": null, + "default_external_id": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "widget_type": "text", + "value_type": "phone", + "minimum": null, + "maximum": null, + "step": null, + "unit": "", + "width": null, + "editors": [], + "optionsets": [], + "conditions": [] + } + }, + { + "model": "questions.question", + "pk": 112, + "fields": { + "created": "2024-06-21T08:14:35.100Z", + "updated": "2024-06-21T08:23:10.154Z", + "uri": "http://example.com/terms/questions/validation/url/url", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/url/url", + "comment": "", + "locked": false, + "attribute": 125, + "is_collection": false, + "is_optional": false, + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "text_lang1": "URL", + "text_lang2": "URL", + "text_lang3": "", + "text_lang4": "", + "text_lang5": "", + "default_text_lang1": "", + "default_text_lang2": "", + "default_text_lang3": "", + "default_text_lang4": "", + "default_text_lang5": "", + "default_option": null, + "default_external_id": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "widget_type": "text", + "value_type": "url", + "minimum": null, + "maximum": null, + "step": null, + "unit": "", + "width": null, + "editors": [], + "optionsets": [], + "conditions": [] + } + }, + { + "model": "questions.question", + "pk": 113, + "fields": { + "created": "2024-06-21T08:16:05.734Z", + "updated": "2024-06-21T08:22:15.506Z", + "uri": "http://example.com/terms/questions/validation/email/email", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/email/email", + "comment": "", + "locked": false, + "attribute": 130, + "is_collection": false, + "is_optional": false, + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "text_lang1": "Email", + "text_lang2": "Email", + "text_lang3": "", + "text_lang4": "", + "text_lang5": "", + "default_text_lang1": "", + "default_text_lang2": "", + "default_text_lang3": "", + "default_text_lang4": "", + "default_text_lang5": "", + "default_option": null, + "default_external_id": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "widget_type": "text", + "value_type": "email", + "minimum": null, + "maximum": null, + "step": null, + "unit": "", + "width": null, + "editors": [], + "optionsets": [], + "conditions": [] + } + }, + { + "model": "questions.question", + "pk": 114, + "fields": { + "created": "2024-06-21T08:16:23.717Z", + "updated": "2024-06-21T08:22:19.882Z", + "uri": "http://example.com/terms/questions/validation/datetime/datetime", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/datetime/datetime", + "comment": "", + "locked": false, + "attribute": 129, + "is_collection": false, + "is_optional": false, + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "text_lang1": "Datetime", + "text_lang2": "Datum und Zeit", + "text_lang3": "", + "text_lang4": "", + "text_lang5": "", + "default_text_lang1": "", + "default_text_lang2": "", + "default_text_lang3": "", + "default_text_lang4": "", + "default_text_lang5": "", + "default_option": null, + "default_external_id": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "widget_type": "text", + "value_type": "datetime", + "minimum": null, + "maximum": null, + "step": null, + "unit": "", + "width": null, + "editors": [], + "optionsets": [], + "conditions": [] + } + }, + { + "model": "questions.question", + "pk": 115, + "fields": { + "created": "2024-06-21T08:16:47.685Z", + "updated": "2024-06-21T08:22:34.329Z", + "uri": "http://example.com/terms/questions/validation/bool/bool", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/bool/bool", + "comment": "", + "locked": false, + "attribute": 132, + "is_collection": false, + "is_optional": false, + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "text_lang1": "Bool", + "text_lang2": "Boolsche Variable", + "text_lang3": "", + "text_lang4": "", + "text_lang5": "", + "default_text_lang1": "", + "default_text_lang2": "", + "default_text_lang3": "", + "default_text_lang4": "", + "default_text_lang5": "", + "default_option": null, + "default_external_id": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "widget_type": "text", + "value_type": "boolean", + "minimum": null, + "maximum": null, + "step": null, + "unit": "", + "width": null, + "editors": [], + "optionsets": [], + "conditions": [] + } + }, + { + "model": "questions.question", + "pk": 116, + "fields": { + "created": "2024-06-21T08:17:12.999Z", + "updated": "2024-06-21T08:22:45.829Z", + "uri": "http://example.com/terms/questions/validation/float/float", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/float/float", + "comment": "", + "locked": false, + "attribute": 126, + "is_collection": false, + "is_optional": false, + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "text_lang1": "Float", + "text_lang2": "Gleitkommazahl", + "text_lang3": "", + "text_lang4": "", + "text_lang5": "", + "default_text_lang1": "", + "default_text_lang2": "", + "default_text_lang3": "", + "default_text_lang4": "", + "default_text_lang5": "", + "default_option": null, + "default_external_id": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "widget_type": "text", + "value_type": "float", + "minimum": null, + "maximum": null, + "step": null, + "unit": "", + "width": null, + "editors": [], + "optionsets": [], + "conditions": [] + } + }, + { + "model": "questions.question", + "pk": 117, + "fields": { + "created": "2024-06-21T08:17:37.732Z", + "updated": "2024-06-21T08:23:00.169Z", + "uri": "http://example.com/terms/questions/validation/integer/integer", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/integer/integer", + "comment": "", + "locked": false, + "attribute": 127, + "is_collection": false, + "is_optional": false, + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "text_lang1": "Integer", + "text_lang2": "Ganzzahl", + "text_lang3": "", + "text_lang4": "", + "text_lang5": "", + "default_text_lang1": "", + "default_text_lang2": "", + "default_text_lang3": "", + "default_text_lang4": "", + "default_text_lang5": "", + "default_option": null, + "default_external_id": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "widget_type": "text", + "value_type": "integer", + "minimum": null, + "maximum": null, + "step": null, + "unit": "", + "width": null, + "editors": [], + "optionsets": [], + "conditions": [] + } + }, + { + "model": "questions.question", + "pk": 118, + "fields": { + "created": "2024-07-18T14:46:30.099Z", + "updated": "2024-07-18T14:47:30.423Z", + "uri": "http://example.com/terms/questions/validation/date/date", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation/date/date", + "comment": "", + "locked": false, + "attribute": 132, + "is_collection": false, + "is_optional": false, + "help_lang1": "", + "help_lang2": "", + "help_lang3": "", + "help_lang4": "", + "help_lang5": "", + "text_lang1": "Date", + "text_lang2": "Datum", + "text_lang3": "", + "text_lang4": "", + "text_lang5": "", + "default_text_lang1": "", + "default_text_lang2": "", + "default_text_lang3": "", + "default_text_lang4": "", + "default_text_lang5": "", + "default_option": null, + "default_external_id": "", + "verbose_name_lang1": "", + "verbose_name_lang2": "", + "verbose_name_lang3": "", + "verbose_name_lang4": "", + "verbose_name_lang5": "", + "widget_type": "text", + "value_type": "date", + "minimum": null, + "maximum": null, + "step": null, + "unit": "", + "width": null, + "editors": [], + "optionsets": [], + "conditions": [] + } + }, { "model": "questions.questionset", "pk": 89, @@ -7324,6 +8335,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "editors": [] } }, @@ -7343,6 +8359,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "editors": [] } }, @@ -7362,6 +8383,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "editors": [] } }, @@ -7381,6 +8407,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "editors": [] } }, @@ -7400,6 +8431,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "editors": [] } }, @@ -7408,17 +8444,22 @@ "pk": 8, "fields": { "created": "2021-05-28T14:35:31.361Z", - "updated": "2021-05-28T14:35:39.559Z", + "updated": "2024-06-20T08:20:53.529Z", "uri": "http://example.com/terms/questions/catalog/blocks", "uri_prefix": "http://example.com/terms", "uri_path": "catalog/blocks", "comment": "", "locked": false, - "title_lang1": "Blocks", - "title_lang2": "Blocks", + "title_lang1": "Blocks of questionsets and questions", + "title_lang2": "Blöcke von Fragensets und Fragen", "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "Blocks", + "short_title_lang2": "Blöcke", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "editors": [] } }, @@ -7438,6 +8479,11 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "editors": [ 2 ] @@ -7459,11 +8505,40 @@ "title_lang3": "", "title_lang4": "", "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", "editors": [ 3 ] } }, + { + "model": "questions.section", + "pk": 11, + "fields": { + "created": "2024-06-21T08:05:56.699Z", + "updated": "2024-07-18T14:45:53.413Z", + "uri": "http://example.com/terms/questions/validation", + "uri_prefix": "http://example.com/terms", + "uri_path": "validation", + "comment": "", + "locked": false, + "title_lang1": "Validation", + "title_lang2": "Validierung", + "title_lang3": "", + "title_lang4": "", + "title_lang5": "", + "short_title_lang1": "", + "short_title_lang2": "", + "short_title_lang3": "", + "short_title_lang4": "", + "short_title_lang5": "", + "editors": [] + } + }, { "model": "questions.sectionpage", "pk": 1, @@ -7913,5 +8988,77 @@ "page": 98, "order": 0 } + }, + { + "model": "questions.sectionpage", + "pk": 51, + "fields": { + "section": 11, + "page": 99, + "order": 0 + } + }, + { + "model": "questions.sectionpage", + "pk": 52, + "fields": { + "section": 11, + "page": 100, + "order": 1 + } + }, + { + "model": "questions.sectionpage", + "pk": 53, + "fields": { + "section": 11, + "page": 101, + "order": 2 + } + }, + { + "model": "questions.sectionpage", + "pk": 54, + "fields": { + "section": 11, + "page": 103, + "order": 3 + } + }, + { + "model": "questions.sectionpage", + "pk": 55, + "fields": { + "section": 11, + "page": 104, + "order": 5 + } + }, + { + "model": "questions.sectionpage", + "pk": 56, + "fields": { + "section": 11, + "page": 105, + "order": 6 + } + }, + { + "model": "questions.sectionpage", + "pk": 57, + "fields": { + "section": 11, + "page": 106, + "order": 7 + } + }, + { + "model": "questions.sectionpage", + "pk": 58, + "fields": { + "section": 11, + "page": 107, + "order": 4 + } } ] diff --git a/testing/manage.py b/testing/manage.py new file mode 100644 index 0000000000..0004541852 --- /dev/null +++ b/testing/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/testing/xml/elements/legacy/catalog-error-key.xml b/testing/xml/elements/legacy/catalog-error-key.xml new file mode 100644 index 0000000000..de962af714 --- /dev/null +++ b/testing/xml/elements/legacy/catalog-error-key.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<rdmo xmlns:dc="http://purl.org/dc/elements/1.1/" created="2023-04-20T09:38:39.297292+02:00"> + <catalog dc:uri="http://example.com/terms/questions/catalog"> + <uri_prefix>http://example.com/terms</uri_prefix> + <uri_path>catalog-legacy-error-title</uri_path> + <dc:comment/> + <order>0</order> + <sections> + <section dc:uri="http://example.com/terms/questions/catalog/individual" order="1"/> + </sections> + </catalog> +</rdmo> diff --git a/testing/xml/elements/updated-and-changed/catalog-1.xml b/testing/xml/elements/updated-and-changed/catalog-1.xml new file mode 100644 index 0000000000..42c62a7a83 --- /dev/null +++ b/testing/xml/elements/updated-and-changed/catalog-1.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<rdmo xmlns:dc="http://purl.org/dc/elements/1.1/" created="2023-04-20T09:38:39.297292+02:00" version="2.0.0"> + <catalog dc:uri="http://example.com/terms/questions/catalog"> + <uri_prefix>http://example.com/terms</uri_prefix> + <uri_path>catalog</uri_path> + <dc:comment/> + <order>0</order> + <title lang="en">Catalog + Lorem mupsimEn dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor endunten ut labore et dolore magna aliquyam erat, sed diam voluptua. At vera eosen3n et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. + Katalog + Lorem spimDe dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor vidididi ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero ededeos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. + +
+
+
+
+
+
+ + + diff --git a/testing/xml/elements/updated-and-changed/optionsets-1.xml b/testing/xml/elements/updated-and-changed/optionsets-1.xml new file mode 100644 index 0000000000..c072d471da --- /dev/null +++ b/testing/xml/elements/updated-and-changed/optionsets-1.xml @@ -0,0 +1,141 @@ + + + + http://example.com/terms + condition + + + + + + + + + + + http://example.com/terms + one_two_three + + + + + + + + + + + + + http://example.com/terms + one_two_three_other + + + + + + + + + + + + + http://example.com/terms + plugin + + simple + + + + diff --git a/testing/xml/error-version.xml b/testing/xml/error-version.xml new file mode 100644 index 0000000000..efaaccaf70 --- /dev/null +++ b/testing/xml/error-version.xml @@ -0,0 +1,3 @@ + + + diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000000..cc71e803bb --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,118 @@ +const webpack = require('webpack') +const { merge } = require('webpack-merge') +const path = require('path') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const TerserPlugin = require('terser-webpack-plugin') + +// list of separate config objects for each django app and their corresponding java script applications +const configList = [ + { + name: 'management', + entry: { + management: [ + './rdmo/management/assets/js/management.js', + './rdmo/management/assets/scss/management.scss' + ] + }, + output: { + filename: 'js/management.js', + path: path.resolve(__dirname, './rdmo/management/static/management/'), + } + }, + { + name: 'projects', + entry: { + projects: [ + './rdmo/projects/assets/js/projects.js', + './rdmo/projects/assets/scss/projects.scss' + ] + }, + output: { + filename: 'js/projects.js', + path: path.resolve(__dirname, './rdmo/projects/static/projects/'), + } + } +] + +// base config for all endpoints +const baseConfig = { + resolve: { + alias: { + rdmo: path.resolve(__dirname, './rdmo/') + }, + extensions: ['*', '.js', '.jsx'] + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'css/[name].css', + chunkFilename: 'css/[id].css' + }), + new webpack.ProvidePlugin({ + $: 'jquery', + jQuery: 'jquery' + }) + ], + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /(node_modules|bower_components)/, + loader: 'babel-loader', + options: { presets: ['@babel/env','@babel/preset-react'] } + }, + { + test: /\.s?css$/, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader', + 'sass-loader' + ] + }, + { + test: /(fonts|files)[\\/].*\.(svg|woff2?|ttf|eot|otf)(\?.*)?$/, + loader: 'file-loader', + type: 'javascript/auto', + options: { + name: '[name].[ext]', + outputPath: 'fonts', + postTransformPublicPath: (p) => `'../' + ${p}`, + esModule: false, + } + } + ] + } +} + +// special config for development +const developmentConfig = { + devtool: 'eval', + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development') + }) + ] +} + +// special config for production +const productionConfig = { + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production') + }) + ], + optimization: { + minimize: true, + minimizer: [new TerserPlugin()] + } +} + +// combine config depending on the provided --mode command line option +module.exports = (env, argv) => { + return configList.map(config => { + if (argv.mode === 'development') { + return merge(config, baseConfig, developmentConfig) + } else { + return merge(config, baseConfig, productionConfig) + } + }) +} diff --git a/webpack/common.config.js b/webpack/common.config.js deleted file mode 100644 index 57f6f89b9b..0000000000 --- a/webpack/common.config.js +++ /dev/null @@ -1,66 +0,0 @@ -const webpack = require('webpack') -const { merge } = require('webpack-merge') -const path = require('path') -const MiniCssExtractPlugin = require('mini-css-extract-plugin') - -const base = { - resolve: { - alias: { - rdmo: path.resolve(__dirname, '../rdmo/') - }, - extensions: ['*', '.js', '.jsx'] - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: 'css/[name].css', - chunkFilename: 'css/[id].css' - }), - new webpack.ProvidePlugin({ - $: 'jquery', - jQuery: 'jquery' - }) - ], - module: { - rules: [ - { - test: /\.(js|jsx)$/, - exclude: /(node_modules|bower_components)/, - loader: 'babel-loader', - options: { presets: ['@babel/env','@babel/preset-react'] } - }, - { - test: /\.s?css$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'sass-loader' - ] - }, - { - test: /(fonts|files)\/.*\.(svg|woff2?|ttf|eot|otf)(\?.*)?$/, - loader: 'file-loader', - options: { - name: '[name].[ext]', - outputPath: 'fonts', - postTransformPublicPath: (p) => `'../' + ${p}` - } - } - ] - } -} - -module.exports = [ - merge(base, { - name: 'management', - entry: { - management: [ - './rdmo/management/assets/js/management.js', - './rdmo/management/assets/scss/management.scss' - ] - }, - output: { - filename: 'js/management.js', - path: path.resolve(__dirname, '../rdmo/management/static/management/'), - } - }) -] diff --git a/webpack/dev.config.js b/webpack/dev.config.js deleted file mode 100644 index adbbf8ac53..0000000000 --- a/webpack/dev.config.js +++ /dev/null @@ -1,15 +0,0 @@ -const webpack = require('webpack') -const { merge } = require('webpack-merge') - -const commonConfig = require('./common.config.js') - -module.exports = commonConfig.map(common => { - return merge(common, { - devtool: 'eval', - plugins: [ - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('development') - }) - ] - }) -}) diff --git a/webpack/prod.config.js b/webpack/prod.config.js deleted file mode 100644 index 2690ef3911..0000000000 --- a/webpack/prod.config.js +++ /dev/null @@ -1,19 +0,0 @@ -const webpack = require('webpack') -const { merge } = require('webpack-merge') -const TerserPlugin = require('terser-webpack-plugin') - -const commonConfig = require('./common.config.js') - -module.exports = commonConfig.map(common => { - return merge(common, { - plugins: [ - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('production') - }) - ], - optimization: { - minimize: true, - minimizer: [new TerserPlugin()] - } - }) -})