From 08171767a02e61b023df80215a2d8806054f3261 Mon Sep 17 00:00:00 2001 From: AJ Date: Fri, 2 Oct 2026 00:43:41 +0200 Subject: [PATCH] Port to Django 5.2 (#635) * Port to Django 5.2 * Pin pluggy to 1.2.0 Any newer and the mystery importlib metadata error appears. Weird! >_> * Update for premailer changed default * Update view logic for is_ajax being changed to a template context processor * Port a few more tests to pytest proper Having two distinct test flavours is giving me a headache * Version 1 dockerfile Makes a VERY big image, I suspect we can optimise this a lot... * Optimise dockerfile a little lot a bit * fix(users): change logout link to POST request * fix(tests): fix some syntax errors in test code still got lots of failed tests :( * fix: replace deprecated Django APIs * ci: update Dockerfile * ci: update Dockerfile * fix(dependencies+tests): update EoL/vulnerable dependencies and improve test stability * Upgrade Python from 3.10 to 3.12 * Update frontend dependencies, replace node-sass with sass * chore(logging): ignore dangling obj reference warning from pypdf * ci: update compose.yml for prod deployment - Add Nginx as a reverse proxy - Add cert-selfsign.sh for generating self-signed certificates - Add certbot-issue.sh for Let's Encrypt certificate issuance and renewal - Add cron-install.sh and cron-uninstall.sh for system cron management - Add .env.example as an environment variable template * style: reformat code * ci: add more sleep trying to pass tests * ci: ignore browser-based tests during ci testing * fix: create home for the new user during Dockerfile building * chore: remove heroku conf file * fix: RIGS not franken anymore * fix: restore is_ajax as a boolean and split out the context processor * test: remove Selenium interaction tests and their CI workarounds * fix: harden production settings (CSRF origins, env casts, ADMINS) and drop dead debug toolbar code * deploy: serve static files from nginx, pin PGDATA, and make cert scripts loud on failure * build: switch .dockerignore to an allowlist * build: drop unused dependencies, soft-pin the rest and target Python 3.14 * build: move image to Python 3.14 / Node 24 and force DEBUG off * ci: add dependabot config for uv, npm, docker and actions * build: narrow Sass deprecation silencing to @import and require Node 24 * build: provide placeholder env for collectstatic now that DEBUG is off in the image * deploy: add plain-HTTP nginx config for local development and use it in compose * deploy: remove self-signed cert script * build: make the image multi-arch with official node and python base images * ci: lint and test on PRs, build the image on PRs and push it to GHCR on master * docs: add local running and sample data instructions * fix: report a form error instead of crashing when big power has no Power MIC * fix: upgrade pypdf and urllib3 to patched releases * fix: apply non-breaking npm audit fixes * fix: strip whitespace in ALLOWED_HOSTS and drop misleading HSTS preload * deploy: add a pyrigs healthcheck and make nginx wait for it * build: pin the uv image version * fix: correct the check-in person picker condition and use BeautifulSoup's string argument * refactor: replace unique_together with UniqueConstraint * build: replace pycodestyle with ruff and fix what it found Removes unused imports and variables, and fixes a few real problems it surfaced: - EventCheckIn.active() referenced an undefined name and raised NameError; it now returns whether the check-in has no end time - RIGS.admin defined EventChecklistAdmin twice; the second is now PowerTestRecordAdmin - RIGS/tests/conftest.py used date/timedelta without importing them - the signal-registering imports in apps.py are kept with noqa pycodestyle config in setup.cfg is dropped. * style: format the codebase with ruff * style: normalise line endings, trailing whitespace and end-of-file newlines * ci: run ruff and file hygiene through prek, and document it --------- Co-authored-by: Hang Co-authored-by: Joe Banks --- .dockerignore | 30 + .env.example | 27 + .github/dependabot.yml | 32 + .github/workflows/ci.yml | 120 + .github/workflows/combine-prs.yml | 4 +- .github/workflows/deploy.yml | 14 - .github/workflows/django.yml | 59 - .gitignore | 12 + .pre-commit-config.yaml | 38 + .python-version | 2 +- .slugignore | 6 - Dockerfile | 68 + LICENSE | 2 +- Procfile | 2 - PyRIGS/decorators.py | 25 +- PyRIGS/formats/en/formats.py | 6 +- PyRIGS/settings.py | 297 +- PyRIGS/tests/base.py | 73 +- PyRIGS/tests/pages.py | 86 - PyRIGS/tests/regions.py | 262 - PyRIGS/tests/test_unit.py | 96 +- PyRIGS/urls.py | 40 +- PyRIGS/views.py | 195 +- PyRIGS/wsgi.py | 4 +- README.md | 83 +- RIGS/admin.py | 110 +- RIGS/apps.py | 4 +- RIGS/forms.py | 179 +- RIGS/management/commands/deleteSampleData.py | 4 +- .../management/commands/generateSampleData.py | 12 +- .../commands/generateSampleRIGSData.py | 505 +- RIGS/management/commands/send_reminders.py | 32 +- RIGS/migrations/0042_auto_20211007_2338.py | 1 - RIGS/models.py | 687 ++- RIGS/signals.py | 65 +- RIGS/static/js/marked.min.js | 2 +- RIGS/templates/base_rigs.html | 30 +- .../email/eventauthorisation_mic_success.txt | 2 +- RIGS/templates/event_detail.html | 14 +- .../templates/eventauthorisation_request.html | 2 +- .../eventauthorisation_request_error.html | 2 +- RIGS/templates/hs/event_checklist_detail.html | 2 +- RIGS/templates/hs/event_checklist_form.html | 2 +- RIGS/templates/hs/eventcheckin_form.html | 12 +- RIGS/templates/hs/hs_object_list.html | 59 + RIGS/templates/hs/power_detail.html | 2 +- RIGS/templates/hs/power_form.html | 2 +- RIGS/templates/hs/power_print.xml | 2 +- RIGS/templates/hs/risk_assessment_detail.html | 2 +- RIGS/templates/hs/risk_assessment_form.html | 2 +- RIGS/templates/partials/contact_details.html | 2 +- RIGS/templates/partials/yes_no_radio.html | 4 +- RIGS/templatetags/filters.py | 93 +- RIGS/templatetags/markdown_tags.py | 47 +- RIGS/tests/conftest.py | 179 +- RIGS/tests/pages.py | 358 -- RIGS/tests/regions.py | 48 - RIGS/tests/test_functional.py | 92 +- RIGS/tests/test_interaction.py | 780 --- RIGS/tests/test_models.py | 108 +- RIGS/tests/test_unit.py | 260 +- RIGS/urls.py | 400 +- RIGS/views/crud.py | 86 +- RIGS/views/finance.py | 179 +- RIGS/views/hs.py | 169 +- RIGS/views/ical.py | 128 +- RIGS/views/rigboard.py | 260 +- app.json | 57 - assets/admin.py | 24 +- assets/apps.py | 4 +- assets/converters.py | 9 +- assets/forms.py | 30 +- .../commands/generateSampleAssetsData.py | 187 +- assets/migrations/0008_auto_20191206_2124.py | 2 +- assets/migrations/0029_unique_constraints.py | 21 + assets/models.py | 89 +- assets/templates/asset_audit.html | 4 +- assets/templates/asset_form.html | 2 +- .../asset_update_search_results.html | 2 +- assets/templates/cable_type_detail.html | 2 +- assets/templatetags/asset_tags.py | 1 - assets/tests/conftest.py | 43 +- assets/tests/pages.py | 251 - assets/tests/test_interaction.py | 358 -- assets/tests/test_unit.py | 107 +- assets/urls.py | 101 +- assets/views.py | 159 +- compose.yml | 110 + conftest.py | 46 +- gulpfile.js | 13 +- nginx/default.conf.example | 46 + nginx/default.dev.conf | 25 + package-lock.json | 4392 ++++------------- package.json | 16 +- pipeline/source_assets/js/asteroids.min.js | 2 +- pipeline/source_assets/scss/email.scss | 1 - pyproject.toml | 142 +- scripts/certbot-issue.sh | 49 + scripts/cron-install.sh | 34 + scripts/cron-uninstall.sh | 19 + setup.cfg | 3 - templates/base.html | 2 +- templates/base_ajax_nomodal.html | 2 +- templates/closemodal.html | 2 +- templates/generic_detail.html | 4 +- templates/pagination.html | 4 +- templates/registration/logged_out.html | 2 +- .../registration/password_change_done.html | 2 +- .../registration/password_reset_complete.html | 4 +- .../registration/password_reset_confirm.html | 12 +- .../registration/password_reset_done.html | 2 +- .../registration/password_reset_form.html | 6 +- .../registration/registration_complete.html | 2 +- templates/search_help.html | 4 +- training/admin.py | 2 +- training/apps.py | 2 +- training/decorators.py | 2 +- training/forms.py | 43 +- .../commands/generateSampleTrainingData.py | 79 +- .../generate_sample_training_users.py | 55 +- training/management/commands/import_old_db.py | 190 +- .../migrations/0008_unique_constraints.py | 45 + training/models.py | 172 +- training/templates/add_level_requirement.html | 4 +- training/templates/edit_training_record.html | 4 +- training/templatetags/tags.py | 22 +- training/tests/conftest.py | 37 +- training/tests/pages.py | 81 - training/tests/test_interaction.py | 80 - training/tests/test_unit.py | 53 +- training/urls.py | 64 +- training/views.py | 119 +- users/forms.py | 17 +- .../commands/generateSampleUserData.py | 161 +- users/management/commands/usercleanup.py | 17 +- users/regbackend.py | 6 +- users/templates/partials/navbar_user.html | 7 +- users/templates/profile_detail.html | 6 +- users/tests/test_users.py | 127 +- users/urls.py | 34 +- users/views.py | 31 +- uv.lock | 1444 +++--- versioning/templates/activity_feed_data.html | 2 +- versioning/tests/test_models.py | 76 +- versioning/tests/test_unit.py | 28 +- versioning/urls.py | 58 +- versioning/versioning.py | 63 +- versioning/views.py | 45 +- 148 files changed, 6573 insertions(+), 9514 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/deploy.yml delete mode 100644 .github/workflows/django.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 .slugignore create mode 100644 Dockerfile delete mode 100644 Procfile delete mode 100644 PyRIGS/tests/pages.py delete mode 100644 PyRIGS/tests/regions.py create mode 100644 RIGS/templates/hs/hs_object_list.html delete mode 100644 RIGS/tests/pages.py delete mode 100644 RIGS/tests/regions.py delete mode 100644 RIGS/tests/test_interaction.py delete mode 100644 app.json create mode 100644 assets/migrations/0029_unique_constraints.py delete mode 100644 assets/tests/pages.py delete mode 100644 assets/tests/test_interaction.py create mode 100644 compose.yml create mode 100644 nginx/default.conf.example create mode 100644 nginx/default.dev.conf create mode 100644 scripts/certbot-issue.sh create mode 100644 scripts/cron-install.sh create mode 100644 scripts/cron-uninstall.sh delete mode 100644 setup.cfg create mode 100644 training/migrations/0008_unique_constraints.py delete mode 100644 training/tests/pages.py delete mode 100644 training/tests/test_interaction.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..ab4728cf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# Exclude everything by default, then explicitly include what the image needs. +# This also keeps .env files, keys and local virtualenvs out of the image. +* + +# Build tooling +!package.json +!package-lock.json +!gulpfile.js +!pyproject.toml +!uv.lock +!.python-version +!manage.py + +# Frontend sources +!pipeline/source_assets/** + +# Django project and apps +!PyRIGS/** +!RIGS/** +!assets/** +!training/** +!users/** +!versioning/** +!templates/** + +# Never ship these, even from within included directories +**/tests +**/__pycache__ +**/*.pyc +**/*.sqlite3 diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..4635ea30 --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +SECRET_KEY=mysecretkey +DEBUG=true +STAGING=false +DJANGO_ALLOWED_HOSTS=localhost +DOMAIN= +SESSION_COOKIE_SECURE_ENABLED=true +CSRF_COOKIE_SECURE_ENABLED=true +FORUM_WEBHOOK_SECRET= + +DATABASE_ENGINE=postgresql +DATABASE_NAME=rigs +DATABASE_USERNAME=rigs +DATABASE_PASSWORD=rigs +DATABASE_HOST=db +DATABASE_PORT=5432 + +EMAIL_HOST= +EMAIL_PORT=587 +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +EMAIL_USE_TLS= +EMAIL_USE_SSL= +EMAIL_FROM= + +HCAPTCHA_SITEKEY= +HCAPTCHA_SECRET= +SENTRY_DSN= diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..ba2f192e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +version: 2 +updates: + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + groups: + python-minor-patch: + update-types: ["minor", "patch"] + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + groups: + npm-minor-patch: + update-types: ["minor", "patch"] + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "docker-compose" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0fea0e31 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,120 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + PYTHONDONTWRITEBYTECODE: 1 + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libcairo2-dev + + - uses: astral-sh/setup-uv@v6 + with: + python-version-file: ".python-version" + enable-cache: true + + - run: uv sync --locked + + - name: Pre-commit hooks (ruff, formatting, file hygiene) + run: uv run prek run --all-files --show-diff-on-failure + + - name: Django system checks + run: uv run python manage.py check + + - name: Check for missing migrations + run: uv run python manage.py makemigrations --check --dry-run + + test: + name: Test + runs-on: ubuntu-latest + env: + DATABASE_ENGINE: sqlite3 + DATABASE_NAME: db.sqlite3 + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: sudo apt-get update && sudo apt-get install -y libcairo2-dev + + - uses: astral-sh/setup-uv@v6 + with: + python-version-file: ".python-version" + enable-cache: true + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - run: uv sync --locked + + - name: Build frontend assets + run: | + npm ci + npm run build + + - run: uv run python manage.py collectstatic --noinput + + - name: Run tests + run: uv run pytest -n auto --cov + + - name: Coveralls + run: uv run coveralls --service=github + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + docker: + name: Docker image + runs-on: ubuntu-latest + needs: [lint, test] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + # Only authenticate when we are actually going to push (never for PRs, which may come from forks) + - name: Log in to GHCR + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=latest + type=sha + + - name: Build (and push on master) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/combine-prs.yml b/.github/workflows/combine-prs.yml index c0a09e05..1d50ee6f 100644 --- a/.github/workflows/combine-prs.yml +++ b/.github/workflows/combine-prs.yml @@ -115,7 +115,7 @@ jobs: core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?'); return; } - + let combinedPRs = []; let mergeFailedPRs = []; for(const { branch, prString } of branchesAndPRStrings) { @@ -133,7 +133,7 @@ jobs: mergeFailedPRs.push(prString); } } - + console.log('Creating combined PR'); const combinedPRsString = combinedPRs.join('\n'); let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString; diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 621e01d2..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Manual Deploy - -on: workflow_dispatch - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: akhileshns/heroku-deploy@v3.12.12 # This is the action - with: - heroku_api_key: ${{secrets.HEROKU_API_KEY}} - heroku_app_name: "pyrigs" #Must be unique in Heroku - heroku_email: "aj@aronajones.com" diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml deleted file mode 100644 index 107e68a2..00000000 --- a/.github/workflows/django.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Django CI - -on: - push: - branches: [master] - pull_request: - branches: [master] - -jobs: - build: - if: "!contains(github.event.head_commit.message, '[ci skip]')" - runs-on: ubuntu-latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PYTHONDONTWRITEBYTECODE: 1 - steps: - - uses: actions/checkout@v4 - - name: Install build dependencies - run: | - sudo apt-get install libcairo2-dev - - - name: "Set up Python" - uses: actions/setup-python@v5 - with: - python-version-file: ".python-version" - - - name: Install uv - uses: astral-sh/setup-uv@v6 - - - name: Install Dependencies - run: uv sync --locked --all-extras --dev - - - name: Cache Static Files - id: static-cache - uses: actions/cache@v4 - with: - path: 'pipeline/built_assets' - key: ${{ hashFiles('package-lock.json') }}-${{ hashFiles('pipeline/source_assets') }} - - - uses: bahmutov/npm-install@v1 - if: steps.static-cache.outputs.cache-hit != 'true' - - run: node node_modules/gulp/bin/gulp build - if: steps.static-cache.outputs.cache-hit != 'true' - - name: Basic Checks - run: | - uv run pycodestyle . --exclude=.venv,migrations,node_modules - uv run python3 manage.py check - uv run python3 manage.py makemigrations --check --dry-run - uv run python3 manage.py collectstatic --noinput - - name: Run Tests - run: uv run pytest -n auto --cov - - uses: actions/upload-artifact@v4 - if: failure() - with: - name: failure-screenshots ${{ matrix.test-group }} - path: screenshots/ - retention-days: 5 - - name: Coveralls - run: uv run coveralls --service=github diff --git a/.gitignore b/.gitignore index 6a078ac5..45899c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,15 @@ screenshots/ # Virutal Environments .venv/ + +# Environment variable files (keep .env.example) +.env +.env.* +!.env.example + +nginx/certs/ +nginx/certbot/ +nginx/*.conf +!nginx/default.dev.conf +*.pem +*.key diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..1aa750d9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + args: [--allow-multiple-documents] + - id: end-of-file-fixer + - id: mixed-line-ending + args: [--fix=lf] + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + + - repo: local + hooks: + - id: uv-check + name: uv lock check + description: Checks the validity of the uv.lock file. + entry: uv lock --check + language: system + files: pyproject.toml + pass_filenames: false + + - id: ruff-lint + name: ruff linting + description: Run ruff linting + entry: uv run ruff check --force-exclude + language: system + 'types_or': [python, pyi] + args: [--fix, --exit-non-zero-on-fix] + + - id: ruff-format + name: ruff formatting + description: Run ruff formatting + entry: uv run ruff format --force-exclude + language: system + 'types_or': [python, pyi] diff --git a/.python-version b/.python-version index c8cfe395..6324d401 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.10 +3.14 diff --git a/.slugignore b/.slugignore deleted file mode 100644 index be7815d3..00000000 --- a/.slugignore +++ /dev/null @@ -1,6 +0,0 @@ -*.sqlite3 -*.md -**/tests -conftest.py -pytest.ini -Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..87811728 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +# Stage 1: build frontend assets (multi-arch: official node image) +FROM node:24-slim AS assets +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY gulpfile.js ./ +COPY pipeline/source_assets ./pipeline/source_assets +RUN npm run build + +# Stage 2: build the Python environment (multi-arch: official python image) +FROM python:3.14-slim-trixie AS builder +COPY --from=ghcr.io/astral-sh/uv:0.12.21 /uv /uvx /bin/ + +# pycairo (via z3c.rml) has no wheels and is compiled against cairo +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential pkg-config libcairo2-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Set up py environment +# DEBUG must never be on in a built image; enable it explicitly via the environment if needed +ENV DEBUG=false \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy + +# Copy uv project files first (for better caching) +COPY pyproject.toml uv.lock ./ + +# Install the project's dependencies using the lockfile and settings +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --frozen --no-install-project --no-dev + +# Then, add the rest of the project source code and install it +# Installing separately from its dependencies allows optimal layer caching +COPY . /app +COPY --from=assets /app/pipeline/built_assets /app/pipeline/built_assets +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev + +# Placeholder values only satisfy settings that are mandatory when DEBUG is off; they are not kept in the image +RUN EMAIL_HOST=build EMAIL_HOST_USER=build EMAIL_HOST_PASSWORD=build EMAIL_FROM=build@example.com \ + uv run python manage.py collectstatic --noinput + +FROM python:3.14-slim-trixie +RUN apt-get update \ + && apt-get install -y --no-install-recommends libcairo2 \ + && rm -rf /var/lib/apt/lists/* +RUN addgroup --system app \ + && adduser --system --group --home /home/app app \ + && mkdir -p /home/app \ + && chown app:app /home/app +COPY --from=builder --chown=app:app /app /app +WORKDIR /app +ENV DEBUG=false \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 +ENV PATH="/app/.venv/bin:$PATH" + +USER app +EXPOSE 8000 +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "PyRIGS.wsgi"] diff --git a/LICENSE b/LICENSE index f080ee7b..7b88283e 100644 --- a/LICENSE +++ b/LICENSE @@ -10,4 +10,4 @@ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Procfile b/Procfile deleted file mode 100644 index 91e43543..00000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ -release: python manage.py migrate -web: gunicorn PyRIGS.wsgi --log-file - diff --git a/PyRIGS/decorators.py b/PyRIGS/decorators.py index fe7d1825..c6b88e66 100644 --- a/PyRIGS/decorators.py +++ b/PyRIGS/decorators.py @@ -9,9 +9,9 @@ from RIGS import models def get_oembed(login_url, request, oembed_view, kwargs): context = {} - context['oembed_url'] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}" - context['login_url'] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}" - resp = render(request, 'login_redirect.html', context=context) + context["oembed_url"] = f"{request.scheme}://{request.META['HTTP_HOST']}{reverse(oembed_view, kwargs=kwargs)}" + context["login_url"] = f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}" + resp = render(request, "login_redirect.html", context=context) return resp @@ -24,7 +24,7 @@ def has_oembed(oembed_view, login_url=settings.LOGIN_URL): if oembed_view is not None: return get_oembed(login_url, request, oembed_view, kwargs) else: - return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}') + return HttpResponseRedirect(f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}") _checklogin.__doc__ = view_func.__doc__ _checklogin.__dict__ = view_func.__dict__ @@ -44,6 +44,7 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None): """ if not login_url: from django.conf import settings + login_url = settings.LOGIN_URL def _dec(view_func): @@ -54,9 +55,9 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None): if oembed_view is not None: return get_oembed(login_url, request, oembed_view, kwargs) else: - return HttpResponseRedirect(f'{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}') + return HttpResponseRedirect(f"{login_url}?{REDIRECT_FIELD_NAME}={request.get_full_path()}") else: - resp = render(request, '403.html') + resp = render(request, "403.html") resp.status_code = 403 return resp @@ -86,10 +87,10 @@ def api_key_required(function): def wrap(request, *args, **kwargs): - userid = kwargs.get('api_pk') - key = kwargs.get('api_key') + userid = kwargs.get("api_pk") + key = kwargs.get("api_key") - error_resp = render(request, '403.html') + error_resp = render(request, "403.html") error_resp.status_code = 403 if key is None: @@ -99,7 +100,7 @@ def api_key_required(function): try: user_object = models.Profile.objects.get(pk=userid) - kwargs = {**kwargs, 'user': user_object} + kwargs = {**kwargs, "user": user_object} except models.Profile.DoesNotExist: return error_resp @@ -117,8 +118,8 @@ def nottinghamtec_address_required(function): def wrap(request, *args, **kwargs): # Fail if current user's email address isn't @nottinghamtec.co.uk - if not request.user.email.endswith('@nottinghamtec.co.uk'): - error_resp = render(request, 'eventauthorisation_request_error.html') + if not request.user.email.endswith("@nottinghamtec.co.uk"): + error_resp = render(request, "eventauthorisation_request_error.html") return error_resp return function(request, *args, **kwargs) diff --git a/PyRIGS/formats/en/formats.py b/PyRIGS/formats/en/formats.py index 0c2f21d6..5d25c48d 100644 --- a/PyRIGS/formats/en/formats.py +++ b/PyRIGS/formats/en/formats.py @@ -1,3 +1,3 @@ -DATETIME_FORMAT = ('d/m/Y H:i') -DATE_FORMAT = ('d/m/Y') -TIME_FORMAT = ('H:i') +DATETIME_FORMAT = "d/m/Y H:i" +DATE_FORMAT = "d/m/Y" +TIME_FORMAT = "H:i" diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index fa992c5a..03bb4fbf 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -10,7 +10,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ import datetime from pathlib import Path -import secrets import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration @@ -20,235 +19,231 @@ from envparse import env BASE_DIR = Path(__file__).resolve(strict=True).parent.parent # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = env('SECRET_KEY', default='gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e') +SECRET_KEY = env("SECRET_KEY", default="gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env('DEBUG', cast=bool, default=True) -STAGING = env('STAGING', cast=bool, default=False) -CI = env('CI', cast=bool, default=False) +DEBUG = env("DEBUG", cast=bool, default=True) +STAGING = env("STAGING", cast=bool, default=False) +CI = env("CI", cast=bool, default=False) -ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] - -if STAGING: - ALLOWED_HOSTS.append('.herokuapp.com') +ALLOWED_HOSTS = [ + host.strip() for host in env("DJANGO_ALLOWED_HOSTS", default="rigs.nottinghamtec.co.uk").split(",") if host.strip() +] if DEBUG: - ALLOWED_HOSTS.append('localhost') - ALLOWED_HOSTS.append('example.com') - ALLOWED_HOSTS.append('127.0.0.1') - ALLOWED_HOSTS.append('.app.github.dev') - CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS + CSRF_TRUSTED_ORIGINS = [f"http://{host}" for host in ALLOWED_HOSTS] + CSRF_TRUSTED_ORIGINS.append("http://localhost:8000") + CSRF_TRUSTED_ORIGINS.append("http://localhost:8001") + ALLOWED_HOSTS = ["*"] +else: + # Django 4+ checks the Origin header on unsafe requests against this list, which + # matters when TLS is terminated by a proxy in front of us. + CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS] -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") if not DEBUG: SECURE_SSL_REDIRECT = True # Redirect all http requests to https + SECURE_HSTS_SECONDS = 3600 + SECURE_HSTS_INCLUDE_SUBDOMAINS = True + SECURE_CONTENT_TYPE_NOSNIFF = True + SESSION_COOKIE_SECURE = env("SESSION_COOKIE_SECURE_ENABLED", cast=bool, default=True) + CSRF_COOKIE_SECURE = env("CSRF_COOKIE_SECURE_ENABLED", cast=bool, default=True) -INTERNAL_IPS = ['127.0.0.1'] +INTERNAL_IPS = ["127.0.0.1"] -DOMAIN = env('DOMAIN', default='example.com') +DOMAIN = env("DOMAIN", default="example.com") -ADMINS = [('IT Manager', f'it@{DOMAIN}'), ('Arona Jones', f'arona.jones@{DOMAIN}')] +ADMINS = [("IT Manager", f"it@{DOMAIN}")] if DEBUG: - ADMINS.append(('Testing Superuser', 'superuser@example.com')) + ADMINS.append(("Testing Superuser", "superuser@example.com")) # Application definition INSTALLED_APPS = ( - 'whitenoise.runserver_nostatic', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - 'versioning', - 'users', - 'RIGS', - 'assets', - 'training', - - # 'debug_toolbar', - 'registration', - 'reversion', - 'widget_tweaks', - 'hcaptcha', - 'massadmin', + "whitenoise.runserver_nostatic", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", + "versioning", + "users", + "RIGS", + "assets", + "training", + "registration", + "reversion", + "widget_tweaks", + "hcaptcha", + "massadmin", ) MIDDLEWARE = ( - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - # 'debug_toolbar.middleware.DebugToolbarMiddleware', - 'reversion.middleware.RevisionMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'htmlmin.middleware.HtmlMinifyMiddleware', - 'htmlmin.middleware.MarkRequestMiddleware', + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "reversion.middleware.RevisionMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "htmlmin.middleware.HtmlMinifyMiddleware", + "htmlmin.middleware.MarkRequestMiddleware", ) -ROOT_URLCONF = 'PyRIGS.urls' +ROOT_URLCONF = "PyRIGS.urls" -WSGI_APPLICATION = 'PyRIGS.wsgi.application' +WSGI_APPLICATION = "PyRIGS.wsgi.application" # Database DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': str(BASE_DIR / 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.{}".format(env("DATABASE_ENGINE", default="sqlite3")), + "NAME": env("DATABASE_NAME", default="rigs"), + "USER": env("DATABASE_USERNAME", default="rigs"), + "PASSWORD": env("DATABASE_PASSWORD", default="rigs"), + "HOST": env("DATABASE_HOST", default="127.0.0.1"), + "PORT": env("DATABASE_PORT", 5432), } } -if not DEBUG: - import dj_database_url - - if env("FRANKENRIGS_DATABASE_URL") is not None: - DATABASES['default'] = dj_database_url.config(env="FRANKENRIGS_DATABASE_URL") - else: - DATABASES['default'] = dj_database_url.config() - # Logging LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': ('%(asctime)s [%(process)d] [%(levelname)s] ' + - 'pathname=%(pathname)s lineno=%(lineno)s ' + - 'funcname=%(funcName)s %(message)s'), - 'datefmt': '%Y-%m-%d %H:%M:%S' + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": ( + "%(asctime)s [%(process)d] [%(levelname)s] " + + "pathname=%(pathname)s lineno=%(lineno)s " + + "funcname=%(funcName)s %(message)s" + ), + "datefmt": "%Y-%m-%d %H:%M:%S", }, - 'simple': { - 'format': '%(levelname)s %(message)s' - } + "simple": {"format": "%(levelname)s %(message)s"}, }, - 'handlers': { - 'null': { - 'level': 'DEBUG', - 'class': 'logging.NullHandler', + "handlers": { + "null": { + "level": "DEBUG", + "class": "logging.NullHandler", }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'verbose' - }, - 'mail_admins': { - 'class': 'django.utils.log.AdminEmailHandler', - 'level': 'ERROR', + "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"}, + "mail_admins": { + "class": "django.utils.log.AdminEmailHandler", + "level": "ERROR", # But the emails are plain text by default - HTML is nicer - 'include_html': True, + "include_html": True, }, }, - 'loggers': { + "loggers": { # Again, default Django configuration to email unhandled exceptions - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, + "django.request": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": True, }, # Might as well log any errors anywhere else in Django - 'django': { - 'handlers': ['console'], - 'level': 'ERROR', - 'propagate': False, + "django": { + "handlers": ["console"], + "level": "ERROR", + "propagate": False, }, - } + # Ignore dangling object references + "pypdf": { + "level": "ERROR", + "propagate": False, + }, + }, } # Tests lock up SQLite otherwise if STAGING or CI: - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' - } - } + CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} elif DEBUG: - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache' - } - } + CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}} else: CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', - 'LOCATION': 'cache_table', + "default": { + "BACKEND": "django.core.cache.backends.db.DatabaseCache", + "LOCATION": "cache_table", } } # Error/performance monitoring sentry_sdk.init( - dsn=env('SENTRY_DSN', default=""), + dsn=env("SENTRY_DSN", default=""), integrations=[DjangoIntegration()], traces_sample_rate=1.0, ) # User system -AUTH_USER_MODEL = 'RIGS.Profile' +AUTH_USER_MODEL = "RIGS.Profile" -LOGIN_REDIRECT_URL = '/' -LOGIN_URL = '/user/login/' -LOGOUT_URL = '/user/logout/' +LOGIN_REDIRECT_URL = "/" +LOGIN_URL = "/user/login/" +LOGOUT_URL = "/user/logout/" ACCOUNT_ACTIVATION_DAYS = 7 # CAPTCHA settings -HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001') -HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000') +HCAPTCHA_SITEKEY = env("HCAPTCHA_SITEKEY", "10000000-ffff-ffff-ffff-000000000001") +HCAPTCHA_SECRET = env("HCAPTCHA_SECRET", "0x0000000000000000000000000000000000000000") # Email EMAILER_TEST = False if not DEBUG or EMAILER_TEST: - EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' - EMAIL_HOST = env('EMAIL_HOST') - EMAIL_PORT = env('EMAIL_PORT', cast=int, default=25) - EMAIL_HOST_USER = env('EMAIL_HOST_USER') - EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') - EMAIL_USE_TLS = env('EMAIL_USE_TLS', cast=bool, default=False) - EMAIL_USE_SSL = env('EMAIL_USE_SSL', cast=bool, default=False) - DEFAULT_FROM_EMAIL = env('EMAIL_FROM') + EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" + EMAIL_HOST = env("EMAIL_HOST") + EMAIL_PORT = env("EMAIL_PORT", cast=int, default=25) + EMAIL_HOST_USER = env("EMAIL_HOST_USER") + EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") + EMAIL_USE_TLS = env("EMAIL_USE_TLS", cast=bool, default=False) + EMAIL_USE_SSL = env("EMAIL_USE_SSL", cast=bool, default=False) + DEFAULT_FROM_EMAIL = env("EMAIL_FROM") else: - EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" EMAIL_COOLDOWN = datetime.timedelta(minutes=15) # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ -LANGUAGE_CODE = 'en-gb' +LANGUAGE_CODE = "en-gb" -TIME_ZONE = 'Europe/London' +TIME_ZONE = "Europe/London" -FORMAT_MODULE_PATH = 'PyRIGS.formats' - -USE_L10N = True +FORMAT_MODULE_PATH = "PyRIGS.formats" USE_TZ = True USE_THOUSAND_SEPARATOR = False # Need to allow seconds as datetime-local input type spits out a time that has seconds -DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S') +DATETIME_INPUT_FORMATS = ("%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S") # Static files (CSS, JavaScript, Images) -STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' -STATIC_URL = '/static/' -STATIC_ROOT = str(BASE_DIR / 'static/') +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} +STATIC_URL = "/static/" +STATIC_ROOT = str(BASE_DIR / "static/") STATICFILES_DIRS = [ - str(BASE_DIR / 'pipeline/built_assets'), + str(BASE_DIR / "pipeline/built_assets"), ] TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - BASE_DIR / 'templates' - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.i18n", @@ -257,8 +252,9 @@ TEMPLATES = [ "django.template.context_processors.tz", "django.template.context_processors.request", "django.contrib.messages.context_processors.messages", + "PyRIGS.views.ajax_context", ], - 'debug': DEBUG + "debug": DEBUG, }, }, ] @@ -266,13 +262,6 @@ TEMPLATES = [ USE_GRAVATAR = True TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf" -AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk' +AUTHORISATION_NOTIFICATION_ADDRESS = "productions@nottinghamtec.co.uk" -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' - -SECURE_HSTS_SECONDS = 3600 -SECURE_HSTS_INCLUDE_SUBDOMAINS = True -SECURE_CONTENT_TYPE_NOSNIFF = True -SESSION_COOKIE_SECURE = env('SESSION_COOKIE_SECURE_ENABLED', True) -CSRF_COOKIE_SECURE = env('CSRF_COOKIE_SECURE_ENABLED', True) -SECURE_HSTS_PRELOAD = True +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/PyRIGS/tests/base.py b/PyRIGS/tests/base.py index bcf1824c..2f96865c 100644 --- a/PyRIGS/tests/base.py +++ b/PyRIGS/tests/base.py @@ -1,16 +1,8 @@ -import os -import pathlib -import sys from datetime import datetime import pytz from django.conf import settings -from django.test import LiveServerTestCase from selenium import webdriver -from selenium.webdriver.support.wait import WebDriverWait - -from RIGS import models as rigsmodels -from . import pages from pytest_django.asserts import assertContains @@ -23,6 +15,7 @@ def create_datetime(year, month, day, hour, minute): def create_browser(): options = webdriver.ChromeOptions() options.add_argument("--window-size=1920,1080") + options.add_argument("--lang=en-GB") options.add_argument("--headless") if settings.CI: options.add_argument("--no-sandbox") @@ -30,76 +23,30 @@ def create_browser(): return driver -class BaseTest(LiveServerTestCase): - def setUp(self): - super().setUpClass() - self.driver = create_browser() - self.wait = WebDriverWait(self.driver, 15) - - def tearDown(self): - super().tearDown() - self.driver.quit() - - -class AutoLoginTest(BaseTest): - def setUp(self): - super().setUp() - self.profile = rigsmodels.Profile( - username="EventTest", first_name="Event", last_name="Test", initials="ETU", is_superuser=True) - self.profile.set_password("EventTestPassword") - self.profile.save() - login_page = pages.LoginPage(self.driver, self.live_server_url).open() - login_page.login("EventTest", "EventTestPassword") - - -# FIXME Refactor as a pytest fixture -def screenshot_failure(func): - def wrapper_func(self, *args, **kwargs): - try: - func(self, *args, **kwargs) - except Exception as e: - screenshot_name = func.__module__ + "." + func.__qualname__ - screenshot_file = "screenshots/" + func.__qualname__ + ".png" - if not pathlib.Path("screenshots").is_dir(): - os.mkdir("screenshots") - self.driver.save_screenshot(screenshot_file) - print(f"Error in test {screenshot_name} is at path {screenshot_file}", file=sys.stderr) - raise e - - return wrapper_func - - -def screenshot_failure_cls(cls): - for attr in cls.__dict__: - if callable(getattr(cls, attr)) and attr.startswith("test"): - setattr(cls, attr, screenshot_failure(getattr(cls, attr))) - return cls - - def assert_times_almost_equal(first_time, second_time): assert first_time.replace(microsecond=0, second=0) == second_time.replace(microsecond=0, second=0) def assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url): # Test the meta tag is in place - response = client.get(event_url, follow=True, HTTP_HOST='example.com') - assertContains(response, 'application/json+oembed') + response = client.get(event_url, follow=True, HTTP_HOST="example.com") + assertContains(response, "application/json+oembed") assertContains(response, oembed_url) # Test that the JSON exists - response = client.get(oembed_url, follow=True, HTTP_HOST='example.com') + response = client.get(oembed_url, follow=True, HTTP_HOST="example.com") assert response.status_code == 200 assertContains(response, event_embed_url) # Should also work for non-existant events - response = client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com') + response = client.get(alt_oembed_url, follow=True, HTTP_HOST="example.com") assert response.status_code == 200 assertContains(response, alt_event_embed_url) def login(client, django_user_model): - pwd = 'testuser' - usr = 'TestUser' - user = django_user_model.objects.create_user(username=usr, email="TestUser@test.com", password=pwd, - is_superuser=True, - is_active=True, is_staff=True) + pwd = "testuser" + usr = "TestUser" + user = django_user_model.objects.create_user( + username=usr, email="TestUser@test.com", password=pwd, is_superuser=True, is_active=True, is_staff=True + ) assert client.login(username=usr, password=pwd) return user diff --git a/PyRIGS/tests/pages.py b/PyRIGS/tests/pages.py deleted file mode 100644 index dec5e20e..00000000 --- a/PyRIGS/tests/pages.py +++ /dev/null @@ -1,86 +0,0 @@ -from pypom import Page -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.common.by import By - -from PyRIGS.tests import regions - - -class BasePage(Page): - form_items = {} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def __getattr__(self, name): - if name in self.form_items: - element = self.form_items[name] - form_element = element[0](self, self.find_element(*element[1])) - return form_element.value - else: - return super().__getattribute__(name) - - def __setattr__(self, name, value): - if name in self.form_items: - element = self.form_items[name] - form_element = element[0](self, self.find_element(*element[1])) - form_element.set_value(value) - else: - self.__dict__[name] = value - - -class FormPage(BasePage): - _errors_selector = (By.CLASS_NAME, "alert-danger") - _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]") - - def remove_all_required(self): - self.driver.execute_script( - "Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});") - self.driver.execute_script( - "Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});") - - def submit(self): - previous_errors = self.errors - submit = self.find_element(*self._submit_locator) - ActionChains(self.driver).move_to_element(submit).perform() - submit.click() - self.wait.until(animation_is_finished()) - self.wait.until(lambda x: self.errors != previous_errors or self.success) - - @property - def errors(self): - try: - error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector)) - return error_page.errors - except NoSuchElementException: - return None - - -class LoginPage(BasePage): - URL_TEMPLATE = '/user/login' - - _username_locator = (By.ID, 'id_username') - _password_locator = (By.ID, 'id_password') - _submit_locator = (By.ID, 'id_submit') - _error_locator = (By.CSS_SELECTOR, '.errorlist>li') - - def login(self, username, password): - username_element = self.find_element(*self._username_locator) - username_element.clear() - username_element.send_keys(username) - - password_element = self.find_element(*self._password_locator) - password_element.clear() - password_element.send_keys(password) - - self.find_element(*self._submit_locator).click() - - -class animation_is_finished(): - def __call__(self, driver): - number_animating = driver.execute_script('return $(":animated").length') - finished = number_animating == 0 - if finished: - import time - time.sleep(0.1) - return finished diff --git a/PyRIGS/tests/regions.py b/PyRIGS/tests/regions.py deleted file mode 100644 index 75afbe81..00000000 --- a/PyRIGS/tests/regions.py +++ /dev/null @@ -1,262 +0,0 @@ -import datetime - -from django.conf import settings -from pypom import Region -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver.common.by import By -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.select import Select - - -def parse_bool_from_string(string): - # Used to convert from attribute strings to boolean values, written after I found this: - # >>> bool("false") - # True - if string == "true": - return True - else: - return False - - -def get_time_format(): - # Default - time_format = "%H%M" - if settings.CI: # The CI is American - time_format = "%I%M%p" - return time_format - - -def get_date_format(): - date_format = "%d%m%Y" - if settings.CI: # And try as I might I can't stop it being so - date_format = "%m%d%Y" - return date_format - - -class BootstrapSelectElement(Region): - _main_button_locator = (By.CSS_SELECTOR, 'button.dropdown-toggle') - _option_box_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu') - _option_locator = (By.CSS_SELECTOR, 'ul.dropdown-menu.inner>li>a.dropdown-item') - _select_all_locator = (By.CLASS_NAME, 'bs-select-all') - _deselect_all_locator = (By.CLASS_NAME, 'bs-deselect-all') - _search_locator = (By.CSS_SELECTOR, '.bs-searchbox>input') - _status_locator = (By.CLASS_NAME, 'status') - - @property - def is_open(self): - return parse_bool_from_string(self.find_element(*self._main_button_locator).get_attribute("aria-expanded")) - - def toggle(self): - original_state = self.is_open - option_box = self.find_element(*self._option_box_locator) - if not original_state: - self.wait.until(expected_conditions.invisibility_of_element(option_box)) - else: - self.wait.until(expected_conditions.visibility_of(option_box)) - return self.find_element(*self._main_button_locator).click() - - def open(self): - if not self.is_open: - self.toggle() - - def close(self): - if self.is_open: - self.toggle() - - def select_all(self): - self.find_element(*self._select_all_locator).click() - - def deselect_all(self): - self.find_element(*self._deselect_all_locator).click() - - def search(self, query): - # self.wait.until(expected_conditions.visibility_of_element_located(self._status_locator)) - search_box = self.find_element(*self._search_locator) - self.open() - search_box.clear() - search_box.send_keys(query) - self.wait.until(expected_conditions.invisibility_of_element_located(self._status_locator)) - - @property - def options(self): - options = list(self.find_elements(*self._option_locator)) - return [self.BootstrapSelectOption(self, i) for i in options] - - def set_option(self, name, selected): - options = [x for x in self.options if x.name == name] - assert len(options) == 1 - options[0].set_selected(selected) - - class BootstrapSelectOption(Region): - _text_locator = (By.CLASS_NAME, 'text') - - @property - def selected(self): - return parse_bool_from_string(self.root.get_attribute("aria-selected")) - - def toggle(self): - self.root.click() - - def set_selected(self, selected): - if self.selected != selected: - self.toggle() - - @property - def name(self): - return self.find_element(*self._text_locator).text - - -class TextBox(Region): - @property - def value(self): - return self.root.get_attribute("value") - - def set_value(self, value): - self.root.clear() - self.root.send_keys(value) - - -class SimpleMDETextArea(Region): - @property - def value(self): - return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().getValue();", self.root.get_attribute("id")) - - def set_value(self, value): - self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.children[1].CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value) - - -class CheckBox(Region): - def toggle(self): - self.root.click() - - @property - def value(self): - return parse_bool_from_string(self.root.get_attribute("checked")) - - def set_value(self, value): - if value != self.value: - self.toggle() - - -class RadioSelect(Region): # Currently only works for yes/no radio selects - def set_value(self, value): - if value: - value = "0" - else: - value = "1" - self.find_element(By.XPATH, f"//label[@for='{self.root.get_attribute('id')}_{value}']").click() - - @property - def value(self): - try: - return parse_bool_from_string(self.find_element(By.CSS_SELECTOR, '.custom-control-input:checked').get_attribute("value").lower()) - except NoSuchElementException: - return None - - -class DatePicker(Region): - @property - def value(self): - return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d") - - def set_value(self, value): - self.root.clear() - self.root.send_keys(value.strftime(get_date_format())) - - -class TimePicker(Region): - @property - def value(self): - return datetime.datetime.strptime(self.root.get_attribute("value"), "%H:%M") - - def set_value(self, value): - self.root.clear() - self.root.send_keys(value.strftime(get_time_format())) - - -class DateTimePicker(Region): - @property - def value(self): - return datetime.datetime.strptime(self.root.get_attribute("value"), "%Y-%m-%d %H:%M") - - def set_value(self, value): - self.root.clear() - - date = value.date().strftime(get_date_format()) - time = value.time().strftime(get_time_format()) - - self.root.send_keys(date) - self.root.send_keys(Keys.TAB) - self.root.send_keys(time) - - -class SingleSelectPicker(Region): - @property - def value(self): - picker = Select(self.root) - return picker.first_selected_option.text - - def set_value(self, value): - picker = Select(self.root) - picker.select_by_visible_text(value) - - -class ErrorPage(Region): - _error_item_selector = (By.CSS_SELECTOR, "dl>span") - - class ErrorItem(Region): - _field_selector = (By.CSS_SELECTOR, "dt") - _error_selector = (By.CSS_SELECTOR, "dd>ul>li") - - @property - def field_name(self): - return self.find_element(*self._field_selector).text - - @property - def errors(self): - return [x.text for x in self.find_elements(*self._error_selector)] - - @property - def errors(self): - error_items = [self.ErrorItem(self, x) for x in self.find_elements(*self._error_item_selector)] - errors = {} - for error in error_items: - errors[error.field_name] = error.errors - return errors - - -class Modal(Region): - _submit_locator = (By.CSS_SELECTOR, '.btn-primary') - _header_selector = (By.TAG_NAME, 'h4') - - form_items = { - 'name': (TextBox, (By.ID, 'id_name')) - } - - @property - def header(self): - return self.find_element(*self._header_selector).text - - @property - def is_open(self): - return self.root.is_displayed() - - def submit(self): - self.root.find_element(*self._submit_locator).click() - - def __getattr__(self, name): - if name in self.form_items: - element = self.form_items[name] - form_element = element[0](self, self.find_element(*element[1])) - return form_element.value - else: - return super().__getattribute__(name) - - def __setattr__(self, name, value): - if name in self.form_items: - element = self.form_items[name] - form_element = element[0](self, self.find_element(*element[1])) - form_element.set_value(value) - else: - self.__dict__[name] = value diff --git a/PyRIGS/tests/test_unit.py b/PyRIGS/tests/test_unit.py index 12207d7d..bbd65dbe 100644 --- a/PyRIGS/tests/test_unit.py +++ b/PyRIGS/tests/test_unit.py @@ -1,21 +1,16 @@ import pytest from django.core.management import call_command -from django.template.defaultfilters import striptags from django.urls import URLPattern, URLResolver from django.urls import reverse from django.urls.exceptions import NoReverseMatch from pytest_django.asserts import assertRedirects, assertContains, assertNotContains -from pytest_django.asserts import assertTemplateUsed, assertInHTML +from pytest_django.asserts import assertTemplateUsed from PyRIGS import urls -from RIGS.models import Event, Profile +from RIGS.models import Event from assets.models import Asset -from training.tests.test_unit import get_response -from django.db import connection -from django.template.defaultfilters import striptags -from django.urls.exceptions import NoReverseMatch -from django.test import TestCase, TransactionTestCase +from django.test import TestCase from django.test.utils import override_settings @@ -36,18 +31,28 @@ def get_request_url(url): try: kwargz = {} if ":pk>" in pattern: - kwargz['pk'] = 1 + kwargz["pk"] = 1 if ":model>" in pattern: - kwargz['model'] = "event" + kwargz["model"] = "event" return reverse(url.name, kwargs=kwargz) except NoReverseMatch: print("Couldn't test url " + pattern) -@pytest.mark.parametrize("command", ['generateSampleAssetsData', 'generateSampleRIGSData', 'generateSampleUserData', - 'deleteSampleData', 'generateSampleTrainingData', 'generate_sample_training_users']) +@pytest.mark.parametrize( + "command", + [ + "generateSampleAssetsData", + "generateSampleRIGSData", + "generateSampleUserData", + "deleteSampleData", + "generateSampleTrainingData", + "generate_sample_training_users", + ], +) def test_production_exception(command): from django.core.management.base import CommandError + with pytest.raises(CommandError, match=".*production"): call_command(command) @@ -55,10 +60,10 @@ def test_production_exception(command): class TestSampleDataGenerator(TestCase): @override_settings(DEBUG=True) def test_sample_data(self): - call_command('generateSampleData') + call_command("generateSampleData") assert Asset.objects.all().count() > 50 assert Event.objects.all().count() > 100 - call_command('deleteSampleData') + call_command("deleteSampleData") assert not Asset.objects.all().exists() assert not Event.objects.all().exists() @@ -66,81 +71,82 @@ class TestSampleDataGenerator(TestCase): @override_settings(DEBUG=True) @pytest.mark.skip(reason="broken") def test_unauthenticated(client): # Nothing should be available to the unauthenticated - call_command('generateSampleData') + call_command("generateSampleData") for url in find_urls_recursive(urls.urlpatterns): request_url = get_request_url(url) - if request_url and 'user' not in request_url: # User module is full of edge cases - response = client.get(request_url, follow=True, HTTP_HOST='example.com') - assertContains(response, 'Login') - if 'application/json+oembed' in response.content.decode(): - assertTemplateUsed(response, 'login_redirect.html') + if request_url and "user" not in request_url: # User module is full of edge cases + response = client.get(request_url, follow=True, HTTP_HOST="example.com") + assertContains(response, "Login") + if "application/json+oembed" in response.content.decode(): + assertTemplateUsed(response, "login_redirect.html") else: if "embed" in str(url): expected_url = f"{reverse('login_embed')}?next={request_url}" else: expected_url = f"{reverse('login')}?next={request_url}" assertRedirects(response, expected_url) - call_command('deleteSampleData') + call_command("deleteSampleData") @override_settings(DEBUG=True) @pytest.mark.skip(reason="broken") def test_basic_access(client): - call_command('generateSampleData') + call_command("generateSampleData") assert client.login(username="basic", password="basic") - url = reverse('asset_list') + url = reverse("asset_list") response = client.get(url) # Check edit and duplicate buttons NOT shown in list - assertNotContains(response, 'Edit') - assertNotContains(response, - 'Duplicate') # If this line is randomly failing, check the debug toolbar HTML hasn't crept in + assertNotContains(response, "Edit") + assertNotContains( + response, "Duplicate" + ) # If this line is randomly failing, check the debug toolbar HTML hasn't crept in - url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id}) + url = reverse("asset_detail", kwargs={"pk": Asset.objects.first().asset_id}) response = client.get(url) - assertNotContains(response, 'Purchase Details') - assertNotContains(response, 'View Revision History') + assertNotContains(response, "Purchase Details") + assertNotContains(response, "View Revision History") - urlz = {'asset_history', 'asset_update', 'asset_duplicate'} + urlz = {"asset_history", "asset_update", "asset_duplicate"} for url_name in urlz: - request_url = reverse(url_name, kwargs={'pk': Asset.objects.first().asset_id}) + request_url = reverse(url_name, kwargs={"pk": Asset.objects.first().asset_id}) response = client.get(request_url, follow=True) assert response.status_code == 403 - request_url = reverse('supplier_create') + request_url = reverse("supplier_create") response = client.get(request_url, follow=True) assert response.status_code == 403 - request_url = reverse('supplier_update', kwargs={'pk': 1}) + request_url = reverse("supplier_update", kwargs={"pk": 1}) response = client.get(request_url, follow=True) assert response.status_code == 403 client.logout() - call_command('deleteSampleData') + call_command("deleteSampleData") @override_settings(DEBUG=True) @pytest.mark.skip(reason="broken") def test_keyholder_access(client): - call_command('generateSampleData') + call_command("generateSampleData") assert client.login(username="keyholder", password="keyholder") - url = reverse('asset_list') + url = reverse("asset_list") response = client.get(url) # Check edit and duplicate buttons shown in list - assertContains(response, 'Edit') - assertContains(response, 'Duplicate') + assertContains(response, "Edit") + assertContains(response, "Duplicate") - url = reverse('asset_detail', kwargs={'pk': Asset.objects.first().asset_id}) + url = reverse("asset_detail", kwargs={"pk": Asset.objects.first().asset_id}) response = client.get(url) - assertContains(response, 'Purchase Details') - assertContains(response, 'View Revision History') + assertContains(response, "Purchase Details") + assertContains(response, "View Revision History") client.logout() - call_command('deleteSampleData') + call_command("deleteSampleData") def test_search(admin_client, admin_user): - url = reverse('search') - response = admin_client.get(url, {'q': "Definetelynothingfoundifwesearchthis"}) + url = reverse("search") + response = admin_client.get(url, {"q": "Definetelynothingfoundifwesearchthis"}) assertContains(response, "No results found") - response = admin_client.get(url, {'q': admin_user.first_name}) + response = admin_client.get(url, {"q": admin_user.first_name}) assertContains(response, admin_user.first_name) diff --git a/PyRIGS/urls.py b/PyRIGS/urls.py index 03d42b50..93b6a513 100644 --- a/PyRIGS/urls.py +++ b/PyRIGS/urls.py @@ -1,43 +1,33 @@ from django.conf import settings -from django.conf.urls import include from django.contrib import admin from django.contrib.auth.decorators import login_required from django.contrib.staticfiles.urls import staticfiles_urlpatterns -from django.urls import path +from django.urls import include, path from django.views.generic import TemplateView from PyRIGS import views urlpatterns = [ - path('', include('versioning.urls')), - path('', include('RIGS.urls')), - path('assets/', include('assets.urls')), - path('training/', include('training.urls')), - - path('', login_required(views.Index.as_view()), name='index'), - + path("", include("versioning.urls")), + path("", include("RIGS.urls")), + path("assets/", include("assets.urls")), + path("training/", include("training.urls")), + path("", login_required(views.Index.as_view()), name="index"), # API - path('api//', login_required(views.SecureAPIRequest.as_view()), - name="api_secure"), - path('api///', login_required(views.SecureAPIRequest.as_view()), - name="api_secure"), - - path('closemodal/', views.CloseModal.as_view(), name='closemodal'), - path('search/', login_required(views.Search.as_view()), name='search'), - path('search_help/', login_required(views.SearchHelp.as_view()), name='search_help'), - - path('', include('users.urls')), - - path('admin/', include('massadmin.urls')), - path('admin/', admin.site.urls), + path("api//", login_required(views.SecureAPIRequest.as_view()), name="api_secure"), + path("api///", login_required(views.SecureAPIRequest.as_view()), name="api_secure"), + path("closemodal/", views.CloseModal.as_view(), name="closemodal"), + path("search/", login_required(views.Search.as_view()), name="search"), + path("search_help/", login_required(views.SearchHelp.as_view()), name="search_help"), + path("", include("users.urls")), + path("admin/", include("massadmin.urls")), + path("admin/", admin.site.urls), path("robots.txt", TemplateView.as_view(template_name="robots.txt", content_type="text/plain")), ] if settings.DEBUG: urlpatterns += staticfiles_urlpatterns() - import debug_toolbar urlpatterns += [ - path('__debug__/', include(debug_toolbar.urls)), - path('bootstrap/', TemplateView.as_view(template_name="bootstrap.html")), + path("bootstrap/", TemplateView.as_view(template_name="bootstrap.html")), ] diff --git a/PyRIGS/views.py b/PyRIGS/views.py index f02da3d2..f0cce9c0 100644 --- a/PyRIGS/views.py +++ b/PyRIGS/views.py @@ -9,11 +9,10 @@ from functools import reduce from itertools import chain from io import BytesIO -from PyPDF2 import PdfFileMerger, PdfFileReader +from pypdf import PdfReader, PdfWriter from z3c.rml import rml2pdf from django.conf import settings -from django.contrib.auth.decorators import login_required from django.contrib import messages from django.core import serializers from django.core.exceptions import PermissionDenied @@ -22,7 +21,6 @@ from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy, reverse, NoReverseMatch from django.views import generic -from django.views.decorators.clickjacking import xframe_options_exempt from django.template.loader import get_template from django.utils import timezone @@ -32,54 +30,65 @@ from training import models as training_models def is_ajax(request): - return request.headers.get('x-requested-with') == 'XMLHttpRequest' + return request.headers.get("x-requested-with") == "XMLHttpRequest" -def get_related(form, context): # Get some other objects to include in the form. Used when there are errors but also nice and quick. +def ajax_context(request): + """Template context processor exposing ``is_ajax`` to every template.""" + return {"is_ajax": is_ajax(request)} + + +def get_related( + form, context +): # Get some other objects to include in the form. Used when there are errors but also nice and quick. for field, model in form.related_models.items(): value = form[field].value() - if value is not None and value != '': + if value is not None and value != "": context[field] = model.objects.get(pk=value) class Index(generic.TemplateView): # Displays the current rig count along with a few other bits and pieces - template_name = 'index.html' + template_name = "index.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['rig_count'] = models.Event.objects.rig_count() - context['now'] = models.Event.objects.events_in_bounds(timezone.now(), timezone.now()).exclude(status=models.Event.CANCELLED).filter(is_rig=True, dry_hire=False) + context["rig_count"] = models.Event.objects.rig_count() + context["now"] = ( + models.Event.objects.events_in_bounds(timezone.now(), timezone.now()) + .exclude(status=models.Event.CANCELLED) + .filter(is_rig=True, dry_hire=False) + ) return context class SecureAPIRequest(generic.View): models = { - 'venue': models.Venue, - 'person': models.Person, - 'organisation': models.Organisation, - 'profile': models.Profile, - 'event': models.Event, - 'asset': asset_models.Asset, - 'supplier': asset_models.Supplier, - 'training_item': training_models.TrainingItem, + "venue": models.Venue, + "person": models.Person, + "organisation": models.Organisation, + "profile": models.Profile, + "event": models.Event, + "asset": asset_models.Asset, + "supplier": asset_models.Supplier, + "training_item": training_models.TrainingItem, } perms = { - 'venue': 'RIGS.view_venue', - 'person': 'RIGS.view_person', - 'organisation': 'RIGS.view_organisation', - 'profile': 'RIGS.view_profile', - 'event': None, - 'asset': None, - 'supplier': None, - 'training_item': None, + "venue": "RIGS.view_venue", + "person": "RIGS.view_person", + "organisation": "RIGS.view_organisation", + "profile": "RIGS.view_profile", + "event": None, + "asset": None, + "supplier": None, + "training_item": None, } - ''' + """ Validate the request is allowed based on user permissions. Raises 403 if denied. Potential to add API key validation at a later date. - ''' + """ def __validate__(self, request, key, perm): if request.user.is_active: @@ -91,16 +100,16 @@ class SecureAPIRequest(generic.View): def get(self, request, model, pk=None, param=None): # Request permission validation things - key = request.GET.get('apikey', None) + key = request.GET.get("apikey", None) perm = self.perms[model] self.__validate__(request, key, perm) # Response format where applicable - format = request.GET.get('format', 'json') - fields = request.GET.get('fields', None) + format = request.GET.get("format", "json") + fields = request.GET.get("fields", None) if fields: fields = fields.split(",") - filters = request.GET.get('filters', []) + filters = request.GET.get("filters", []) if filters: filters = filters.split(",") @@ -111,10 +120,10 @@ class SecureAPIRequest(generic.View): return HttpResponse(data, content_type="application/" + format) # Supply data for autocomplete ajax request in json form - term = request.GET.get('q', None) + term = request.GET.get("q", None) if term: if fields is None: # Default to just name - fields = ['name'] + fields = ["name"] # Build a list of Q objects for use later queries = [] @@ -138,14 +147,14 @@ class SecureAPIRequest(generic.View): if model == "profile": objects = objects.filter(is_active=True, is_approved=True) for o in objects: - name = o.display_name if hasattr(o, 'display_name') else o.name + name = o.display_name if hasattr(o, "display_name") else o.name data = { - 'pk': o.pk, - 'value': o.pk, - 'text': name, + "pk": o.pk, + "value": o.pk, + "text": name, } try: # See if there is a valid update URL - data['update'] = reverse(f"{model}_update", kwargs={'pk': o.pk}) + data["update"] = reverse(f"{model}_update", kwargs={"pk": o.pk}) except NoReverseMatch: pass results.append(data) @@ -153,8 +162,8 @@ class SecureAPIRequest(generic.View): # return a data response return JsonResponse(results, safe=False) - start = request.GET.get('start', None) - end = request.GET.get('end', None) + start = request.GET.get("start", None) + end = request.GET.get("end", None) if model == "event" and start and end: # Probably a calendar request @@ -166,13 +175,13 @@ class SecureAPIRequest(generic.View): results = [] for item in objects: data = { - 'pk': item.pk, - 'title': item.name, - 'is_rig': item.is_rig, - 'status': str(item.get_status_display()), - 'earliest': item.earliest_time.isoformat(), - 'latest': item.latest_time.isoformat(), - 'url': str(item.get_absolute_url()) + "pk": item.pk, + "title": item.name, + "is_rig": item.is_rig, + "status": str(item.get_status_display()), + "earliest": item.earliest_time.isoformat(), + "latest": item.latest_time.isoformat(), + "url": str(item.get_absolute_url()), } results.append(data) @@ -184,32 +193,35 @@ class SecureAPIRequest(generic.View): class ModalURLMixin: def get_close_url(self, update, detail): if is_ajax(self.request): - url = reverse_lazy('closemodal') - update_url = str(reverse_lazy(update, kwargs={'pk': self.object.pk})) + url = reverse_lazy("closemodal") + update_url = str(reverse_lazy(update, kwargs={"pk": self.object.pk})) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) messages.info(self.request, f"modalobject[0]['update_url']='{update_url}'") else: - url = reverse_lazy(detail, kwargs={ - 'pk': self.object.pk, - }) + url = reverse_lazy( + detail, + kwargs={ + "pk": self.object.pk, + }, + ) return url class GenericListView(generic.ListView): - template_name = 'generic_list.html' + template_name = "generic_list.html" paginate_by = 20 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = self.model.__name__ + "s" + context["page_title"] = self.model.__name__ + "s" if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" return context def get_queryset(self): - object_list = self.model.objects.search(query=self.request.GET.get('q', "")) + object_list = self.model.objects.search(query=self.request.GET.get("q", "")) - orderBy = self.request.GET.get('orderBy', "name") + orderBy = self.request.GET.get("orderBy", "name") if orderBy != "": object_list = object_list.order_by(orderBy) return object_list @@ -220,9 +232,9 @@ class GenericDetailView(generic.DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"{self.model.__name__} | {self.object.name}" + context["page_title"] = f"{self.model.__name__} | {self.object.name}" if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" return context @@ -231,9 +243,9 @@ class GenericUpdateView(generic.UpdateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"Edit {self.model.__name__}" + context["page_title"] = f"Edit {self.model.__name__}" if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" return context @@ -242,27 +254,27 @@ class GenericCreateView(generic.CreateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"Create {self.model.__name__}" + context["page_title"] = f"Create {self.model.__name__}" if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" return context class Search(generic.ListView): - template_name = 'search_results.html' + template_name = "search_results.html" paginate_by = 20 count = 0 def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - context['count'] = self.count or 0 - context['query'] = self.request.GET.get('q') - context['page_title'] = f"{context['count']} search results for {context['query']}" + context["count"] = self.count or 0 + context["query"] = self.request.GET.get("q") + context["page_title"] = f"{context['count']} search results for {context['query']}" return context def get_queryset(self): request = self.request - query = request.GET.get('q', None) + query = request.GET.get("q", None) if query is not None: event_results = models.Event.objects.search(query) @@ -287,16 +299,14 @@ class Search(generic.ListView): trainee_results, training_item_results, ) - qs = sorted(queryset_chain, - key=lambda instance: instance.pk, - reverse=True) + qs = sorted(queryset_chain, key=lambda instance: instance.pk, reverse=True) self.count = len(qs) # since qs is actually a list return qs return models.Event.objects.none() # just an empty queryset as default class SearchHelp(generic.TemplateView): - template_name = 'search_help.html' + template_name = "search_help.html" class CloseModal(generic.TemplateView): @@ -305,10 +315,11 @@ class CloseModal(generic.TemplateView): May optionally also include some javascript in a success message to cause a load of the new information onto the page. """ - template_name = 'closemodal.html' + + template_name = "closemodal.html" def get_context_data(self, **kwargs): - return {'messages': messages.get_messages(self.request)} + return {"messages": messages.get_messages(self.request)} class OEmbedView(generic.View): @@ -317,10 +328,10 @@ class OEmbedView(generic.View): full_url = f"{request.scheme}://{request.META['HTTP_HOST']}{embed_url}" data = { - 'html': f'', - 'version': '1.0', - 'type': 'rich', - 'height': '250' + "html": f'', + "version": "1.0", + "type": "rich", + "height": "250", } return JsonResponse(data) @@ -328,15 +339,15 @@ class OEmbedView(generic.View): def get_info_string(user): user_str = f"by {user.name} " if user else "" - time = timezone.now().strftime('%d/%m/%Y %H:%I') + time = timezone.now().strftime("%d/%m/%Y %H:%I") return f"[Paperwork generated {user_str}on {time}" def render_pdf_response(template, context, append_terms): - merger = PdfFileMerger() + merger = PdfWriter() rml = template.render(context) buffer = rml2pdf.parseString(rml) - merger.append(PdfFileReader(buffer)) + merger.append(PdfReader(buffer)) buffer.close() if append_terms: @@ -346,9 +357,9 @@ def render_pdf_response(template, context, append_terms): merged = BytesIO() merger.write(merged) - response = HttpResponse(content_type='application/pdf') - f = context['filename'] - response['Content-Disposition'] = f'filename="{f}"' + response = HttpResponse(content_type="application/pdf") + f = context["filename"] + response["Content-Disposition"] = f'filename="{f}"' response.write(merged.getvalue()) return response @@ -357,14 +368,14 @@ class PrintView(generic.View): append_terms = False def get_context_data(self, **kwargs): - obj = get_object_or_404(self.model, pk=self.kwargs['pk']) - object_name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', obj.name) + obj = get_object_or_404(self.model, pk=self.kwargs["pk"]) + object_name = re.sub(r"[^a-zA-Z0-9 \n\.]", "", obj.name) context = { - 'object': obj, - 'current_user': self.request.user, - 'object_name': object_name, - 'info_string': get_info_string(self.request.user) + f"- {obj.current_version_id}]", + "object": obj, + "current_user": self.request.user, + "object_name": object_name, + "info_string": get_info_string(self.request.user) + f"- {obj.current_version_id}]", } return context @@ -376,8 +387,8 @@ class PrintView(generic.View): class PrintListView(generic.ListView): def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) - context['current_user'] = self.request.user - context['info_string'] = get_info_string(self.request.user) + "]" + context["current_user"] = self.request.user + context["info_string"] = get_info_string(self.request.user) + "]" return context def get(self, request): diff --git a/PyRIGS/wsgi.py b/PyRIGS/wsgi.py index ff4fa810..619c5014 100644 --- a/PyRIGS/wsgi.py +++ b/PyRIGS/wsgi.py @@ -6,11 +6,11 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ + import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "PyRIGS.settings") from django.core.wsgi import get_wsgi_application # noqa -from dj_static import Cling # noqa -application = Cling(get_wsgi_application()) +application = get_wsgi_application() diff --git a/README.md b/README.md index b54c9c95..ca91749d 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,64 @@ -# TEC PA & Lighting - PyRIGS # -![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg) -[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS) -[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability) - -Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets. - -For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki) - -# Apps -- PyRIGS: Base app, stores 'global' information -- RIGS: Rigboard stuff - event calendar etc -- assets: Database of our kit, testing data etc -- training: Logs in-house training within various "departments" (sound, lighting etc). -- versioning: Our custom logic built on top of django-reversion. Semi-modular. -- users: Our custom logic for registration and profiles. Semi-modular. - - -[![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com) +# TEC PA & Lighting - PyRIGS # +![Build Status](https://github.com/nottinghamtec/PyRIGS/workflows/Django%20CI/badge.svg) +[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg)](https://coveralls.io/github/nottinghamtec/PyRIGS) +[![Maintainability](https://api.codeclimate.com/v1/badges/79ca3b8106911a1d143f/maintainability)](https://codeclimate.com/github/nottinghamtec/PyRIGS/maintainability) + +Welcome to TEC PA & Lighting's PyRIGS program. This is a reimplementation of the previous Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. PyRIGS is our in house app for the centralisation of information on our events and now assets. + +For setup information and other such helpful stuff check the [Wiki](https://github.com/nottinghamtec/PyRIGS/wiki) + +# Apps +- PyRIGS: Base app, stores 'global' information +- RIGS: Rigboard stuff - event calendar etc +- assets: Database of our kit, testing data etc +- training: Logs in-house training within various "departments" (sound, lighting etc). +- versioning: Our custom logic built on top of django-reversion. Semi-modular. +- users: Our custom logic for registration and profiles. Semi-modular. + +# Running locally +The compose stack runs the app behind a plain-HTTP nginx (`nginx/default.dev.conf`) on port 80. + +1. Copy `.env.example` to `.env` and fill it in. For local use set at least: +2. Start the stack (migrations run automatically on start): + ``` + docker compose up -d --build + ``` +3. Open . + +## Creating a user +``` +docker compose exec pyrigs python manage.py createsuperuser +``` + +## Sample data +Load sample users, events, assets and training data (run once on a fresh database): +``` +docker compose exec pyrigs python manage.py generateSampleData +``` +The individual commands (`generateSampleUserData`, `generateSampleRIGSData`, `generateSampleAssetsData`, `generateSampleTrainingData`) can be run separately, but run the user one first. To remove the sample data again (requires `DEBUG=true`): +``` +docker compose exec pyrigs python manage.py deleteSampleData +``` + +The sample data creates these logins (the password is the same as the username): + +| Username | Password | Access | +|---|---|---| +| `superuser` | `superuser` | Superuser | +| `finance` | `finance` | Finance and Keyholders groups | +| `hs` | `hs` | H&S and Keyholders groups | +| `keyholder` | `keyholder` | Keyholders group | +| `basic` | `basic` | Approved user, no groups | + +It also creates a number of generic profiles (e.g. `AmyPond`, `RoryWilliams`) which have no password set, so they can't be logged into. + + +# Development +Linting and formatting use [ruff](https://docs.astral.sh/ruff/), run through [prek](https://github.com/j178/prek) (a pre-commit compatible hook runner) using `.pre-commit-config.yaml`: +``` +uv sync +uv run prek install # run the hooks on every commit +uv run prek run --all-files +``` + +[![forthebadge](https://forthebadge.com/images/badges/built-with-resentment.svg)](https://forthebadge.com) [![forthebadge](https://forthebadge.com/images/badges/contains-technical-debt.svg)](https://forthebadge.com) diff --git a/RIGS/admin.py b/RIGS/admin.py index 375594bc..f5b2d51c 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -32,15 +32,11 @@ def merge_model_instances(primary_object, alias_objects): """ # get related fields - related_fields = list(filter( - lambda x: x.is_relation is True, - primary_object._meta.get_fields())) + related_fields = list(filter(lambda x: x.is_relation is True, primary_object._meta.get_fields())) - many_to_many_fields = list(filter( - lambda x: x.many_to_many is True, related_fields)) + many_to_many_fields = list(filter(lambda x: x.many_to_many is True, related_fields)) - related_fields = list(filter( - lambda x: x.many_to_many is False, related_fields)) + related_fields = list(filter(lambda x: x.many_to_many is False, related_fields)) # Loop through all alias objects and migrate their references to the # primary object @@ -69,10 +65,7 @@ def merge_model_instances(primary_object, alias_objects): through_model_instances = through_model.objects.filter(**kwargs) for instance in through_model_instances: # Re-attach the through model to the primary_object - setattr( - instance, - many_to_many_field.m2m_field_name(), - primary_object) + setattr(instance, many_to_many_field.m2m_field_name(), primary_object) instance.save() # TODO: Here, try to delete duplicate instances that are # disallowed by a unique_together constraint @@ -109,31 +102,34 @@ def merge_model_instances(primary_object, alias_objects): class AssociateAdmin(VersionAdmin): - search_fields = ['id', 'name'] - list_display_links = ['id', 'name'] - actions = ['merge'] + search_fields = ["id", "name"] + list_display_links = ["id", "name"] + actions = ["merge"] def get_queryset(self, request): - return super().get_queryset(request).annotate(event_count=Count('event')) + return super().get_queryset(request).annotate(event_count=Count("event")) def number_of_events(self, obj): return obj.latest_events.count() - number_of_events.admin_order_field = 'event_count' + number_of_events.admin_order_field = "event_count" def merge(self, request, queryset): - if request.POST.get('post'): # Has the user confirmed which is the master record? + if request.POST.get("post"): # Has the user confirmed which is the master record? try: - master_object_pk = request.POST.get('master') + master_object_pk = request.POST.get("master") master_object = queryset.get(pk=master_object_pk) except ObjectDoesNotExist: self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR) return - primary_object, deleted_objects, deleted_objects_count = merge_model_instances(master_object, queryset.exclude(pk=master_object_pk).all()) - reversion.set_comment('Merging Objects') + primary_object, deleted_objects, deleted_objects_count = merge_model_instances( + master_object, queryset.exclude(pk=master_object_pk).all() + ) + reversion.set_comment("Merging Objects") self.message_user(request, f"Objects successfully merged. {deleted_objects_count} old objects deleted.") else: # Present the confirmation screen + class TempForm(ModelForm): class Meta: model = queryset.model @@ -144,39 +140,51 @@ class AssociateAdmin(VersionAdmin): forms.append(TempForm(instance=obj)) context = { - 'title': _("Are you sure?"), - 'queryset': queryset, - 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, - 'forms': forms + "title": _("Are you sure?"), + "queryset": queryset, + "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME, + "forms": forms, } - return TemplateResponse(request, 'admin_associate_merge.html', context) + return TemplateResponse(request, "admin_associate_merge.html", context) @admin.register(models.Profile) class ProfileAdmin(UserAdmin, AssociateAdmin): - list_display = ('username', 'name', 'is_approved', 'is_superuser', 'is_supervisor', 'number_of_events', 'last_login', 'date_joined') - list_display_links = ['username'] - list_filter = UserAdmin.list_filter + ('is_approved', 'date_joined') + list_display = ( + "username", + "name", + "is_approved", + "is_superuser", + "is_supervisor", + "number_of_events", + "last_login", + "date_joined", + ) + list_display_links = ["username"] + list_filter = UserAdmin.list_filter + ("is_approved", "date_joined") fieldsets = ( - (None, {'fields': ('username', 'password')}), - (_('Personal info'), { - 'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}), - (_('Permissions'), {'fields': ('is_approved', 'is_active', 'is_staff', 'is_superuser', - 'groups', 'user_permissions')}), - (_('Important dates'), { - 'fields': ('last_login', 'date_joined')}), + (None, {"fields": ("username", "password")}), + (_("Personal info"), {"fields": ("first_name", "last_name", "email", "initials", "phone")}), + ( + _("Permissions"), + {"fields": ("is_approved", "is_active", "is_staff", "is_superuser", "groups", "user_permissions")}, + ), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), ) add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('username', 'password1', 'password2'), - }), + ( + None, + { + "classes": ("wide",), + "fields": ("username", "password1", "password2"), + }, + ), ) form = user_forms.ProfileChangeForm add_form = user_forms.ProfileCreationForm - actions = ['approve_user', 'merge'] + actions = ["approve_user", "merge"] - merge_fields = ['username', 'first_name', 'last_name', 'initials', 'email', 'phone', 'is_supervisor'] + merge_fields = ["username", "first_name", "last_name", "initials", "email", "phone", "is_supervisor"] def approve_user(modeladmin, request, queryset): queryset.update(is_approved=True) @@ -184,32 +192,32 @@ class ProfileAdmin(UserAdmin, AssociateAdmin): @admin.register(models.Person) class PersonAdmin(AssociateAdmin): - list_display = ('id', 'name', 'phone', 'email', 'number_of_events') - merge_fields = ['name', 'phone', 'email', 'address', 'notes'] + list_display = ("id", "name", "phone", "email", "number_of_events") + merge_fields = ["name", "phone", "email", "address", "notes"] @admin.register(models.Venue) class VenueAdmin(AssociateAdmin): - list_display = ('id', 'name', 'phone', 'email', 'number_of_events') - merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available'] + list_display = ("id", "name", "phone", "email", "number_of_events") + merge_fields = ["name", "phone", "email", "address", "notes", "three_phase_available"] @admin.register(models.Organisation) class OrganisationAdmin(AssociateAdmin): - list_display = ('id', 'name', 'phone', 'email', 'number_of_events') - merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account'] + list_display = ("id", "name", "phone", "email", "number_of_events") + merge_fields = ["name", "phone", "email", "address", "notes", "union_account"] @admin.register(models.RiskAssessment) class RiskAssessmentAdmin(VersionAdmin): - list_display = ('id', 'event', 'reviewed_at', 'reviewed_by') + list_display = ("id", "event", "reviewed_at", "reviewed_by") @admin.register(models.EventChecklist) class EventChecklistAdmin(VersionAdmin): - list_display = ('id', 'event', 'reviewed_at', 'reviewed_by') + list_display = ("id", "event", "reviewed_at", "reviewed_by") @admin.register(models.PowerTestRecord) -class EventChecklistAdmin(VersionAdmin): - list_display = ('id', 'event', 'reviewed_at', 'reviewed_by') +class PowerTestRecordAdmin(VersionAdmin): + list_display = ("id", "event", "reviewed_at", "reviewed_by") diff --git a/RIGS/apps.py b/RIGS/apps.py index a0dc3724..4b44be5a 100644 --- a/RIGS/apps.py +++ b/RIGS/apps.py @@ -2,7 +2,7 @@ from django.apps import AppConfig class RIGSAppConfig(AppConfig): - name = 'RIGS' + name = "RIGS" def ready(self): - import RIGS.signals + import RIGS.signals # noqa: F401 (registers signal handlers) diff --git a/RIGS/forms.py b/RIGS/forms.py index e2e62fca..a9780c8c 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta import simplejson from django import forms @@ -6,15 +6,14 @@ from django.conf import settings from django.core import serializers from django.utils import timezone from django.utils.html import format_html -from reversion import revisions as reversion from RIGS import models from training.models import TrainingLevel # Override the django form defaults to use the HTML date/time/datetime UI elements -forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) -forms.TimeField.widget = forms.TimeInput(attrs={'type': 'time'}, format='%H:%M') -forms.DateTimeField.widget = forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%d %H:%M') +forms.DateField.widget = forms.DateInput(attrs={"type": "date"}) +forms.TimeField.widget = forms.TimeInput(attrs={"type": "time"}, format="%H:%M") +forms.DateTimeField.widget = forms.DateTimeInput(attrs={"type": "datetime-local"}, format="%Y-%m-%d %H:%M") # Events Shit @@ -22,25 +21,29 @@ class EventForm(forms.ModelForm): datetime_input_formats = list(settings.DATETIME_INPUT_FORMATS) meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False) access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False) - parking_and_access = forms.BooleanField(label="Additional parking or access requirements (i.e. campus parking permits, wristbands)?", required=False) + parking_and_access = forms.BooleanField( + label="Additional parking or access requirements (i.e. campus parking permits, wristbands)?", required=False + ) items_json = forms.CharField() items = {} related_models = { - 'person': models.Person, - 'organisation': models.Organisation, - 'venue': models.Venue, - 'mic': models.Profile, - 'checked_in_by': models.Profile, + "person": models.Person, + "organisation": models.Organisation, + "venue": models.Venue, + "mic": models.Profile, + "checked_in_by": models.Profile, } @property def _get_items_json(self): items = {} + if self.instance.pk is None: + return items for item in self.instance.items.all(): - data = serializers.serialize('json', [item]) + data = serializers.serialize("json", [item]) struct = simplejson.loads(data) items[item.pk] = simplejson.dumps(struct[0]) return simplejson.dumps(items) @@ -48,23 +51,23 @@ class EventForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['items_json'].initial = self._get_items_json - self.fields['start_date'].widget.format = '%Y-%m-%d' - self.fields['end_date'].widget.format = '%Y-%m-%d' + self.fields["items_json"].initial = self._get_items_json + self.fields["start_date"].widget.format = "%Y-%m-%d" + self.fields["end_date"].widget.format = "%Y-%m-%d" - self.fields['access_at'].widget.format = '%Y-%m-%dT%H:%M:%S' - self.fields['meet_at'].widget.format = '%Y-%m-%dT%H:%M:%S' + self.fields["access_at"].widget.format = "%Y-%m-%dT%H:%M:%S" + self.fields["meet_at"].widget.format = "%Y-%m-%dT%H:%M:%S" def init_items(self): self.items = self.process_items_json() return self.items def process_items_json(self, event=None): - data = simplejson.loads(self.cleaned_data['items_json']) + data = simplejson.loads(self.cleaned_data["items_json"]) items = {} for key in data: pk = int(key) - items[pk] = self._get_or_initialise_item(pk, data[key]['fields'], event) + items[pk] = self._get_or_initialise_item(pk, data[key]["fields"], event) return items @@ -79,35 +82,45 @@ class EventForm(forms.ModelForm): item = models.EventItem() # Take the data from the form and update the item object - item.name = data['name'] - item.description = data['description'] - item.quantity = data['quantity'] - item.cost = data['cost'] - item.order = data['order'] + item.name = data["name"] + item.description = data["description"] + item.quantity = data["quantity"] + item.cost = data["cost"] + item.order = data["order"] - if (event): + if event: item.event = event item.full_clean() else: - item.full_clean('event') + item.full_clean("event") return item def clean(self): if self.cleaned_data.get("is_rig") and not ( - self.cleaned_data.get('person') or self.cleaned_data.get('organisation')): + self.cleaned_data.get("person") or self.cleaned_data.get("organisation") + ): raise forms.ValidationError( - 'You haven\'t provided any client contact details. Please add a person or organisation.', - code='contact') + "You haven't provided any client contact details. Please add a person or organisation.", code="contact" + ) access = self.cleaned_data.get("access_at") - if 'warn-access' not in self.data and access is not None and access.date() < (self.cleaned_data.get("start_date") - timedelta(days=7)): - raise forms.ValidationError(format_html("Are you sure about that? Your access time seems a bit optimistic. If you're sure, save again. "), code='access_sanity') + if ( + "warn-access" not in self.data + and access is not None + and access.date() < (self.cleaned_data.get("start_date") - timedelta(days=7)) + ): + raise forms.ValidationError( + format_html( + "Are you sure about that? Your access time seems a bit optimistic. If you're sure, save again. " + ), + code="access_sanity", + ) return super().clean() def save(self, commit=True): m = super().save(commit=False) - if (commit): + if commit: m.save() cur_items = m.items.all() items = self.process_items_json(m) @@ -123,10 +136,29 @@ class EventForm(forms.ModelForm): class Meta: model = models.Event - fields = ['is_rig', 'name', 'venue', 'start_time', 'end_date', 'start_date', - 'end_time', 'meet_at', 'access_at', 'description', 'notes', 'mic', - 'person', 'organisation', 'dry_hire', 'checked_in_by', 'status', - 'purchase_order', 'collector', 'forum_url', 'parking_and_access'] + fields = [ + "is_rig", + "name", + "venue", + "start_time", + "end_date", + "start_date", + "end_time", + "meet_at", + "access_at", + "description", + "notes", + "mic", + "person", + "organisation", + "dry_hire", + "checked_in_by", + "status", + "purchase_order", + "collector", + "forum_url", + "parking_and_access", + ] class BaseClientEventAuthorisationForm(forms.ModelForm): @@ -134,8 +166,8 @@ class BaseClientEventAuthorisationForm(forms.ModelForm): name = forms.CharField(label="Your Name") def clean(self): - if self.cleaned_data.get('amount') != self.instance.event.total: - self.add_error('amount', 'The amount authorised must equal the total for the event (inc VAT).') + if self.cleaned_data.get("amount") != self.instance.event.total: + self.add_error("amount", "The amount authorised must equal the total for the event (inc VAT).") return super().clean() class Meta: @@ -145,70 +177,81 @@ class BaseClientEventAuthorisationForm(forms.ModelForm): class InternalClientEventAuthorisationForm(BaseClientEventAuthorisationForm): def __init__(self, **kwargs): super().__init__(**kwargs) - self.fields['uni_id'].required = True - self.fields['account_code'].required = True + self.fields["uni_id"].required = True + self.fields["account_code"].required = True class Meta: model = models.EventAuthorisation - fields = ('tos', 'name', 'amount', 'uni_id', 'account_code') + fields = ("tos", "name", "amount", "uni_id", "account_code") class EventAuthorisationRequestForm(forms.Form): - email = forms.EmailField(required=True, label='Authoriser Email') + email = forms.EmailField(required=True, label="Authoriser Email") class EventRiskAssessmentForm(forms.ModelForm): related_models = { - 'power_mic': models.Profile, + "power_mic": models.Profile, } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for name, field in self.fields.items(): - if str(name) == 'supervisor_consulted': + if str(name) == "supervisor_consulted": field.widget = forms.CheckboxInput() elif field.__class__ == forms.BooleanField: - field.widget = forms.RadioSelect(choices=[ - (True, 'Yes'), - (False, 'No') - ], attrs={'class': 'custom-control-input', 'required': 'true'}) + field.widget = forms.RadioSelect( + choices=[(True, "Yes"), (False, "No")], attrs={"class": "custom-control-input", "required": "true"} + ) def clean(self): - if self.cleaned_data.get('big_power'): - if not self.cleaned_data.get('power_mic').level_qualifications.filter(level__department=TrainingLevel.POWER).exists(): - self.add_error('power_mic', forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required")) + if self.cleaned_data.get("big_power"): + power_mic = self.cleaned_data.get("power_mic") + if power_mic is None: + self.add_error( + "power_mic", + forms.ValidationError("A Power MIC is required for big power.", code="power_mic_required"), + ) + elif not power_mic.level_qualifications.filter(level__department=TrainingLevel.POWER).exists(): + self.add_error( + "power_mic", + forms.ValidationError("Your Power MIC must be a Power Technician.", code="power_tech_required"), + ) # Check expected values unexpected_values = [] for field, value in models.RiskAssessment.expected_values.items(): if self.cleaned_data.get(field) != value: unexpected_values.append(f"
  • {self._meta.model._meta.get_field(field).help_text}
  • ") - if len(unexpected_values) > 0 and not self.cleaned_data.get('supervisor_consulted'): - raise forms.ValidationError(f"Your answers to these questions:
      {''.join([str(elem) for elem in unexpected_values])}
    require consulting with a supervisor.", code='unusual_answers') + if len(unexpected_values) > 0 and not self.cleaned_data.get("supervisor_consulted"): + raise forms.ValidationError( + f"Your answers to these questions:
      {''.join([str(elem) for elem in unexpected_values])}
    require consulting with a supervisor.", + code="unusual_answers", + ) return super().clean() class Meta: model = models.RiskAssessment - fields = '__all__' - exclude = ['reviewed_at', 'reviewed_by'] + fields = "__all__" + exclude = ["reviewed_at", "reviewed_by"] class EventChecklistForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['date'].widget.format = '%Y-%m-%d' + self.fields["date"].widget.format = "%Y-%m-%d" for name, field in self.fields.items(): if field.__class__ == forms.NullBooleanField: # Only display yes/no to user, the 'none' is only ever set in the background field.widget = forms.CheckboxInput() related_models = { - 'venue': models.Venue, + "venue": models.Venue, } class Meta: model = models.EventChecklist - fields = '__all__' - exclude = ['reviewed_at', 'reviewed_by'] + fields = "__all__" + exclude = ["reviewed_at", "reviewed_by"] class PowerTestRecordForm(forms.ModelForm): @@ -220,29 +263,29 @@ class PowerTestRecordForm(forms.ModelForm): field.widget = forms.CheckboxInput() related_models = { - 'venue': models.Venue, - 'power_mic': models.Profile, + "venue": models.Venue, + "power_mic": models.Profile, } class Meta: model = models.PowerTestRecord - fields = '__all__' - exclude = ['reviewed_at', 'reviewed_by'] + fields = "__all__" + exclude = ["reviewed_at", "reviewed_by"] class EventCheckInForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['time'].initial = timezone.now() - self.fields['role'].initial = "Crew" + self.fields["time"].initial = timezone.now() + self.fields["role"].initial = "Crew" class Meta: model = models.EventCheckIn - fields = '__all__' - exclude = ['end_time'] + fields = "__all__" + exclude = ["end_time"] class EditCheckInForm(forms.ModelForm): class Meta: model = models.EventCheckIn - fields = '__all__' + fields = "__all__" diff --git a/RIGS/management/commands/deleteSampleData.py b/RIGS/management/commands/deleteSampleData.py index 1e996a45..d5be2926 100644 --- a/RIGS/management/commands/deleteSampleData.py +++ b/RIGS/management/commands/deleteSampleData.py @@ -7,13 +7,13 @@ from training import models as tmodels class Command(BaseCommand): - help = 'Deletes testing sample data' + help = "Deletes testing sample data" def handle(self, *args, **kwargs): from django.conf import settings if not settings.DEBUG: - raise CommandError('You cannot run this command in production') + raise CommandError("You cannot run this command in production") self.delete_objects(models.AssetCategory) self.delete_objects(models.AssetStatus) diff --git a/RIGS/management/commands/generateSampleData.py b/RIGS/management/commands/generateSampleData.py index 237c0cb0..9b7357e4 100644 --- a/RIGS/management/commands/generateSampleData.py +++ b/RIGS/management/commands/generateSampleData.py @@ -1,15 +1,13 @@ from django.core.management import call_command from django.core.management.base import BaseCommand -from RIGS import models - class Command(BaseCommand): - help = 'Adds sample data to use for testing' + help = "Adds sample data to use for testing" can_import_settings = True def handle(self, *args, **options): - call_command('generateSampleUserData') - call_command('generateSampleRIGSData') - call_command('generateSampleAssetsData') - call_command('generateSampleTrainingData') + call_command("generateSampleUserData") + call_command("generateSampleRIGSData") + call_command("generateSampleAssetsData") + call_command("generateSampleTrainingData") diff --git a/RIGS/management/commands/generateSampleRIGSData.py b/RIGS/management/commands/generateSampleRIGSData.py index 1292fb67..69db214d 100644 --- a/RIGS/management/commands/generateSampleRIGSData.py +++ b/RIGS/management/commands/generateSampleRIGSData.py @@ -1,7 +1,6 @@ import datetime import random -from django.contrib.auth.models import Group, Permission from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.utils import timezone @@ -11,7 +10,7 @@ from RIGS import models class Command(BaseCommand): - help = 'Adds sample data to use for testing' + help = "Adds sample data to use for testing" can_import_settings = True people = [] @@ -25,13 +24,14 @@ class Command(BaseCommand): from django.conf import settings if not (settings.DEBUG or settings.STAGING): - raise CommandError('You cannot run this command in production') + raise CommandError("You cannot run this command in production") random.seed( - 'Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests + "Some object to seed the random number generator" + ) # otherwise it is done by time, which could lead to inconsistant tests with transaction.atomic(): - models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + models.VatRate.objects.create(start_at="2014-03-05", rate=0.20, comment="test1") self.setup_people() self.setup_organisations() self.setup_venues() @@ -39,18 +39,72 @@ class Command(BaseCommand): print("Done generating rigboard data") def setup_people(self): - names = ["Regulus Black", "Sirius Black", "Lavender Brown", "Cho Chang", "Vincent Crabbe", "Vincent Crabbe", - "Bartemius Crouch", "Fleur Delacour", "Cedric Diggory", "Alberforth Dumbledore", "Albus Dumbledore", - "Dudley Dursley", "Petunia Dursley", "Vernon Dursley", "Argus Filch", "Seamus Finnigan", - "Nicolas Flamel", "Cornelius Fudge", "Goyle", "Gregory Goyle", "Hermione Granger", "Rubeus Hagrid", - "Igor Karkaroff", "Viktor Krum", "Bellatrix Lestrange", "Alice Longbottom", "Frank Longbottom", - "Neville Longbottom", "Luna Lovegood", "Xenophilius Lovegood", # noqa - "Remus Lupin", "Draco Malfoy", "Lucius Malfoy", "Narcissa Malfoy", "Olympe Maxime", - "Minerva McGonagall", "Mad-Eye Moody", "Peter Pettigrew", "Harry Potter", "James Potter", - "Lily Potter", "Quirinus Quirrell", "Tom Riddle", "Mary Riddle", "Lord Voldemort", "Rita Skeeter", - "Severus Snape", "Nymphadora Tonks", "Dolores Janes Umbridge", "Arthur Weasley", "Bill Weasley", - "Charlie Weasley", "Fred Weasley", "George Weasley", "Ginny Weasley", "Molly Weasley", "Percy Weasley", - "Ron Weasley", "Dobby", "Fluffy", "Hedwig", "Moaning Myrtle", "Aragog", "Grawp"] # noqa + names = [ + "Regulus Black", + "Sirius Black", + "Lavender Brown", + "Cho Chang", + "Vincent Crabbe", + "Vincent Crabbe", + "Bartemius Crouch", + "Fleur Delacour", + "Cedric Diggory", + "Alberforth Dumbledore", + "Albus Dumbledore", + "Dudley Dursley", + "Petunia Dursley", + "Vernon Dursley", + "Argus Filch", + "Seamus Finnigan", + "Nicolas Flamel", + "Cornelius Fudge", + "Goyle", + "Gregory Goyle", + "Hermione Granger", + "Rubeus Hagrid", + "Igor Karkaroff", + "Viktor Krum", + "Bellatrix Lestrange", + "Alice Longbottom", + "Frank Longbottom", + "Neville Longbottom", + "Luna Lovegood", + "Xenophilius Lovegood", # noqa + "Remus Lupin", + "Draco Malfoy", + "Lucius Malfoy", + "Narcissa Malfoy", + "Olympe Maxime", + "Minerva McGonagall", + "Mad-Eye Moody", + "Peter Pettigrew", + "Harry Potter", + "James Potter", + "Lily Potter", + "Quirinus Quirrell", + "Tom Riddle", + "Mary Riddle", + "Lord Voldemort", + "Rita Skeeter", + "Severus Snape", + "Nymphadora Tonks", + "Dolores Janes Umbridge", + "Arthur Weasley", + "Bill Weasley", + "Charlie Weasley", + "Fred Weasley", + "George Weasley", + "Ginny Weasley", + "Molly Weasley", + "Percy Weasley", + "Ron Weasley", + "Dobby", + "Fluffy", + "Hedwig", + "Moaning Myrtle", + "Aragog", + "Grawp", + ] # noqa for i, name in enumerate(names): with reversion.create_revision(): reversion.set_user(random.choice(models.Profile.objects.all())) @@ -72,32 +126,131 @@ class Command(BaseCommand): self.people.append(person) def setup_organisations(self): - names = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", - "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", - "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", - "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", - "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", - "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", - "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", - "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", - "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", - "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", - "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", - "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", - "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa - "Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", - "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", - "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", - "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", - "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", - "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", - "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", - "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", - "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", - "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", - "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", - "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", - "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa + names = [ + "Acme, inc.", + "Widget Corp", + "123 Warehousing", + "Demo Company", + "Smith and Co.", + "Foo Bars", + "ABC Telecom", + "Fake Brothers", + "QWERTY Logistics", + "Demo, inc.", + "Sample Company", + "Sample, inc", + "Acme Corp", + "Allied Biscuit", + "Ankh-Sto Associates", + "Extensive Enterprise", + "Galaxy Corp", + "Globo-Chem", + "Mr. Sparkle", + "Globex Corporation", + "LexCorp", + "LuthorCorp", + "North Central Positronics", + "Omni Consimer Products", + "Praxis Corporation", + "Sombra Corporation", + "Sto Plains Holdings", + "Tessier-Ashpool", + "Wayne Enterprises", + "Wentworth Industries", + "ZiffCorp", + "Bluth Company", + "Strickland Propane", + "Thatherton Fuels", + "Three Waters", + "Water and Power", + "Western Gas & Electric", + "Mammoth Pictures", + "Mooby Corp", + "Gringotts", + "Thrift Bank", + "Flowers By Irene", + "The Legitimate Businessmens Club", + "Osato Chemicals", + "Transworld Consortium", + "Universal Export", + "United Fried Chicken", + "Virtucon", + "Kumatsu Motors", + "Keedsler Motors", + "Powell Motors", + "Industrial Automation", + "Sirius Cybernetics Corporation", + "U.S. Robotics and Mechanical Men", + "Colonial Movers", + "Corellian Engineering Corporation", + "Incom Corporation", + "General Products", + "Leeding Engines Ltd.", + "Blammo", # noqa + "Input, Inc.", + "Mainway Toys", + "Videlectrix", + "Zevo Toys", + "Ajax", + "Axis Chemical Co.", + "Barrytron", + "Carrys Candles", + "Cogswell Cogs", + "Spacely Sprockets", + "General Forge and Foundry", + "Duff Brewing Company", + "Dunder Mifflin", + "General Services Corporation", + "Monarch Playing Card Co.", + "Krustyco", + "Initech", + "Roboto Industries", + "Primatech", + "Sonky Rubber Goods", + "St. Anky Beer", + "Stay Puft Corporation", + "Vandelay Industries", + "Wernham Hogg", + "Gadgetron", + "Burleigh and Stronginthearm", + "BLAND Corporation", + "Nordyne Defense Dynamics", + "Petrox Oil Company", + "Roxxon", + "McMahon and Tate", + "Sixty Second Avenue", + "Charles Townsend Agency", + "Spade and Archer", + "Megadodo Publications", + "Rouster and Sideways", + "C.H. Lavatory and Sons", + "Globo Gym American Corp", + "The New Firm", + "SpringShield", + "Compuglobalhypermeganet", + "Data Systems", + "Gizmonic Institute", + "Initrode", + "Taggart Transcontinental", + "Atlantic Northern", + "Niagular", + "Plow King", + "Big Kahuna Burger", + "Big T Burgers and Fries", + "Chez Quis", + "Chotchkies", + "The Frying Dutchman", + "Klimpys", + "The Krusty Krab", + "Monks Diner", + "Milliways", + "Minuteman Cafe", + "Taco Grande", + "Tip Top Cafe", + "Moes Tavern", + "Central Perk", + "Chasers", + ] # noqa for i, name in enumerate(names): with reversion.create_revision(): reversion.set_user(random.choice(models.Profile.objects.all())) @@ -113,7 +266,9 @@ class Command(BaseCommand): new_organisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua" if i % 7 == 0: - new_organisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567" + new_organisation.address = ( + "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567" + ) if i % 9 == 0: new_organisation.phone = "01234 567894" @@ -122,18 +277,76 @@ class Command(BaseCommand): self.organisations.append(new_organisation) def setup_venues(self): - names = ["Bear Island", "Crossroads Inn", "Deepwood Motte", "The Dreadfort", "The Eyrie", "Greywater Watch", - "The Iron Islands", "Karhold", "Moat Cailin", "Oldstones", "Raventree Hall", "Riverlands", - "The Ruby Ford", "Saltpans", "Seagard", "Torrhen's Square", "The Trident", "The Twins", - "The Vale of Arryn", "The Whispering Wood", "White Harbor", "Winterfell", "The Arbor", "Ashemark", - "Brightwater Keep", "Casterly Rock", "Clegane's Keep", "Dragonstone", "Dorne", "God's Eye", - "The Golden Tooth", # noqa - "Harrenhal", "Highgarden", "Horn Hill", "Fingers", "King's Landing", "Lannisport", "Oldtown", - "Rainswood", "Storm's End", "Summerhall", "Sunspear", "Tarth", "Castle Black", "Craster's Keep", - "Fist of the First Men", "The Frostfangs", "The Gift", "The Skirling Pass", "The Wall", "Asshai", - "Astapor", "Braavos", "The Dothraki Sea", "Lys", "Meereen", "Myr", "Norvos", "Pentos", "Qarth", - "Qohor", "The Red Waste", "Tyrosh", "Vaes Dothrak", "Valyria", "Village of the Lhazareen", "Volantis", - "Yunkai"] # noqa + names = [ + "Bear Island", + "Crossroads Inn", + "Deepwood Motte", + "The Dreadfort", + "The Eyrie", + "Greywater Watch", + "The Iron Islands", + "Karhold", + "Moat Cailin", + "Oldstones", + "Raventree Hall", + "Riverlands", + "The Ruby Ford", + "Saltpans", + "Seagard", + "Torrhen's Square", + "The Trident", + "The Twins", + "The Vale of Arryn", + "The Whispering Wood", + "White Harbor", + "Winterfell", + "The Arbor", + "Ashemark", + "Brightwater Keep", + "Casterly Rock", + "Clegane's Keep", + "Dragonstone", + "Dorne", + "God's Eye", + "The Golden Tooth", # noqa + "Harrenhal", + "Highgarden", + "Horn Hill", + "Fingers", + "King's Landing", + "Lannisport", + "Oldtown", + "Rainswood", + "Storm's End", + "Summerhall", + "Sunspear", + "Tarth", + "Castle Black", + "Craster's Keep", + "Fist of the First Men", + "The Frostfangs", + "The Gift", + "The Skirling Pass", + "The Wall", + "Asshai", + "Astapor", + "Braavos", + "The Dothraki Sea", + "Lys", + "Meereen", + "Myr", + "Norvos", + "Pentos", + "Qarth", + "Qohor", + "The Red Waste", + "Tyrosh", + "Vaes Dothrak", + "Valyria", + "Village of the Lhazareen", + "Volantis", + "Yunkai", + ] # noqa for i, name in enumerate(names): with reversion.create_revision(): reversion.set_user(random.choice(self.profiles)) @@ -158,32 +371,81 @@ class Command(BaseCommand): self.venues.append(new_venue) def setup_events(self): - names = ["Outdoor Concert", "Hall Open Mic Night", "Festival", "Weekend Event", "Magic Show", "Society Ball", - "Evening Show", "Talent Show", "Acoustic Evening", "Hire of Things", "SU Event", - "End of Term Show", "Theatre Show", "Outdoor Fun Day", "Summer Carnival", "Open Days", "Magic Show", - "Awards Ceremony", "Debating Event", "Club Night", "DJ Evening", "Building Projection", - "Choir Concert"] - descriptions = ["A brief description of the event", "This event is boring", "Probably wont happen", - "Warning: this has lots of kit"] - notes = ["The client came into the office at some point", "Who knows if this will happen", - "Probably should check this event", "Maybe not happening", "Run away!"] + names = [ + "Outdoor Concert", + "Hall Open Mic Night", + "Festival", + "Weekend Event", + "Magic Show", + "Society Ball", + "Evening Show", + "Talent Show", + "Acoustic Evening", + "Hire of Things", + "SU Event", + "End of Term Show", + "Theatre Show", + "Outdoor Fun Day", + "Summer Carnival", + "Open Days", + "Magic Show", + "Awards Ceremony", + "Debating Event", + "Club Night", + "DJ Evening", + "Building Projection", + "Choir Concert", + ] + descriptions = [ + "A brief description of the event", + "This event is boring", + "Probably wont happen", + "Warning: this has lots of kit", + ] + notes = [ + "The client came into the office at some point", + "Who knows if this will happen", + "Probably should check this event", + "Maybe not happening", + "Run away!", + ] item_options = [ - {'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, - 'cost': 200.00}, - {'name': 'Projector', - 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', - 'quantity': 1, 'cost': 500.00}, - {'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, - 'cost': 200.52}, - {'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00}, - {'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, - 'cost': 0.50}, - {'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, - 'cost': 100.00}, - {'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00}, - {'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00}, - {'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}] + { + "name": "Speakers", + "description": "Some really really big speakers \n these are very loud", + "quantity": 2, + "cost": 200.00, + }, + { + "name": "Projector", + "description": "Some kind of video thinamejig, probably with unnecessary processing for free", + "quantity": 1, + "cost": 500.00, + }, + { + "name": "Lighting Desk", + "description": "Cannot provide guarentee that it will work", + "quantity": 1, + "cost": 200.52, + }, + {"name": "Moving lights", "description": "Flashy lights, with the copper", "quantity": 8, "cost": 50.00}, + { + "name": "Microphones", + "description": "Make loud noise \n you will want speakers with this", + "quantity": 5, + "cost": 0.50, + }, + { + "name": "Sound Mixer Thing", + "description": "Might be analogue, might be digital", + "quantity": 1, + "cost": 100.00, + }, + {"name": "Electricity", "description": "You need this", "quantity": 1, "cost": 200.00}, + {"name": "Crew", "description": "Costs nothing, because reasons", "quantity": 1, "cost": 0.00}, + {"name": "Loyalty Discount", "description": "Have some negative moneys", "quantity": 1, "cost": -50.00}, + ] day_delta = -120 # start adding events from 4 months ago @@ -222,9 +484,10 @@ class Command(BaseCommand): # Could have any status, equally weighted new_event.status = random.choice( - [models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED]) + [models.Event.BOOKED, models.Event.CONFIRMED, models.Event.PROVISIONAL, models.Event.CANCELLED] + ) - new_event.dry_hire = (random.randint(0, 7) == 0) # 1 in 7 are dry hire + new_event.dry_hire = random.randint(0, 7) == 0 # 1 in 7 are dry hire if random.randint(0, 1) > 0: # 1 in 2 have description new_event.description = random.choice(descriptions) @@ -253,39 +516,49 @@ class Command(BaseCommand): if new_event.status is models.Event.CANCELLED: # void cancelled events new_invoice.void = True elif random.randint(0, 2) > 1: # 1 in 3 have been paid - models.Payment.objects.create(invoice=new_invoice, amount=new_invoice.balance, - date=datetime.date.today(), method=random.choice(models.Payment.METHODS)[0]) + models.Payment.objects.create( + invoice=new_invoice, + amount=new_invoice.balance, + date=datetime.date.today(), + method=random.choice(models.Payment.METHODS)[0], + ) if i == 1 or random.randint(0, 5) > 0: # Event 1 and 1 in 5 have a RA - models.RiskAssessment.objects.create(event=new_event, supervisor_consulted=bool(random.getrandbits(1)), - nonstandard_equipment=bool(random.getrandbits(1)), - nonstandard_use=bool(random.getrandbits(1)), - contractors=bool(random.getrandbits(1)), - other_companies=bool(random.getrandbits(1)), - crew_fatigue=bool(random.getrandbits(1)), - big_power=bool(random.getrandbits(1)), - generators=bool(random.getrandbits(1)), - other_companies_power=bool(random.getrandbits(1)), - nonstandard_equipment_power=bool(random.getrandbits(1)), - multiple_electrical_environments=bool(random.getrandbits(1)), - noise_monitoring=bool(random.getrandbits(1)), - known_venue=bool(random.getrandbits(1)), - safe_loading=bool(random.getrandbits(1)), - safe_storage=bool(random.getrandbits(1)), - area_outside_of_control=bool(random.getrandbits(1)), - barrier_required=bool(random.getrandbits(1)), - nonstandard_emergency_procedure=bool(random.getrandbits(1)), - special_structures=bool(random.getrandbits(1)), - suspended_structures=bool(random.getrandbits(1)), - parking_and_access=bool(random.getrandbits(1)), - outside=bool(random.getrandbits(1))) + models.RiskAssessment.objects.create( + event=new_event, + supervisor_consulted=bool(random.getrandbits(1)), + nonstandard_equipment=bool(random.getrandbits(1)), + nonstandard_use=bool(random.getrandbits(1)), + contractors=bool(random.getrandbits(1)), + other_companies=bool(random.getrandbits(1)), + crew_fatigue=bool(random.getrandbits(1)), + big_power=bool(random.getrandbits(1)), + generators=bool(random.getrandbits(1)), + other_companies_power=bool(random.getrandbits(1)), + nonstandard_equipment_power=bool(random.getrandbits(1)), + multiple_electrical_environments=bool(random.getrandbits(1)), + noise_monitoring=bool(random.getrandbits(1)), + known_venue=bool(random.getrandbits(1)), + safe_loading=bool(random.getrandbits(1)), + safe_storage=bool(random.getrandbits(1)), + area_outside_of_control=bool(random.getrandbits(1)), + barrier_required=bool(random.getrandbits(1)), + nonstandard_emergency_procedure=bool(random.getrandbits(1)), + special_structures=bool(random.getrandbits(1)), + suspended_structures=bool(random.getrandbits(1)), + parking_and_access=bool(random.getrandbits(1)), + outside=bool(random.getrandbits(1)), + ) if i == 0 or random.randint(0, 1) > 0: # Event 1 and 1 in 10 have a Checklist - models.EventChecklist.objects.create(event=new_event, - safe_parking=bool(random.getrandbits(1)), - safe_packing=bool(random.getrandbits(1)), - exits=bool(random.getrandbits(1)), - trip_hazard=bool(random.getrandbits(1)), - warning_signs=bool(random.getrandbits(1)), - ear_plugs=bool(random.getrandbits(1)), - hs_location="Locked away safely", - extinguishers_location="Somewhere, I forgot", - date=timezone.now(), venue=random.choice(self.venues)) + models.EventChecklist.objects.create( + event=new_event, + safe_parking=bool(random.getrandbits(1)), + safe_packing=bool(random.getrandbits(1)), + exits=bool(random.getrandbits(1)), + trip_hazard=bool(random.getrandbits(1)), + warning_signs=bool(random.getrandbits(1)), + ear_plugs=bool(random.getrandbits(1)), + hs_location="Locked away safely", + extinguishers_location="Somewhere, I forgot", + date=timezone.now(), + venue=random.choice(self.venues), + ) diff --git a/RIGS/management/commands/send_reminders.py b/RIGS/management/commands/send_reminders.py index 4f38d734..467eae87 100644 --- a/RIGS/management/commands/send_reminders.py +++ b/RIGS/management/commands/send_reminders.py @@ -4,7 +4,7 @@ import datetime from django.template.loader import get_template from django.contrib.staticfiles import finders from django.conf import settings -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.core.mail import EmailMultiAlternatives from django.utils import timezone from django.urls import reverse @@ -13,17 +13,27 @@ from RIGS import models class Command(BaseCommand): - help = 'Sends email reminders as required. Triggered daily through heroku-scheduler in production.' + help = "Sends email reminders as required. Triggered daily through sys cron in production." def handle(self, *args, **options): - events = models.Event.objects.current_events().select_related('riskassessment') + events = models.Event.objects.current_events().select_related("riskassessment") for event in events: - earliest_time = event.earliest_time if isinstance(event.earliest_time, datetime.datetime) else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00))) + earliest_time = ( + event.earliest_time + if isinstance(event.earliest_time, datetime.datetime) + else timezone.make_aware(datetime.datetime.combine(event.earliest_time, datetime.time(00, 00))) + ) # 48 hours = 172800 seconds - if event.is_rig and not event.cancelled and not event.dry_hire and (earliest_time - timezone.now()).total_seconds() <= 172800 and not hasattr(event, 'riskassessment'): + if ( + event.is_rig + and not event.cancelled + and not event.dry_hire + and (earliest_time - timezone.now()).total_seconds() <= 172800 + and not hasattr(event, "riskassessment") + ): context = { "event": event, - "url": "https://" + settings.DOMAIN + reverse('event_ra', kwargs={'pk': event.pk}) + "url": "https://" + settings.DOMAIN + reverse("event_ra", kwargs={"pk": event.pk}), } target = event.mic.email if event.mic else f"productions@{settings.DOMAIN}" msg = EmailMultiAlternatives( @@ -32,7 +42,11 @@ class Command(BaseCommand): to=[target], reply_to=[f"h.s.manager@{settings.DOMAIN}"], ) - css = finders.find('css/email.css') - html = premailer.Premailer(get_template("email/ra_reminder.html").render(context), external_styles=css).transform() - msg.attach_alternative(html, 'text/html') + css = finders.find("css/email.css") + html = premailer.Premailer( + get_template("email/ra_reminder.html").render(context), + external_styles=css, + allow_loading_external_files=True, + ).transform() + msg.attach_alternative(html, "text/html") msg.send() diff --git a/RIGS/migrations/0042_auto_20211007_2338.py b/RIGS/migrations/0042_auto_20211007_2338.py index 20343cdf..4b83450e 100644 --- a/RIGS/migrations/0042_auto_20211007_2338.py +++ b/RIGS/migrations/0042_auto_20211007_2338.py @@ -31,4 +31,3 @@ class Migration(migrations.Migration): field=models.DecimalField(blank=True, decimal_places=2, help_text='Earth Fault Loop Impedance (ZS)', max_digits=5, null=True, verbose_name='Earth Fault Loop Impedance'), ), ] - diff --git a/RIGS/models.py b/RIGS/models.py index ecdf96a2..2b564800 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -8,7 +8,7 @@ from urllib.parse import urlparse import pytz from django import forms -from django.db.models import Q, F +from django.db.models import Q from django.conf import settings from django.contrib.auth.models import AbstractUser from django.core.exceptions import ValidationError @@ -17,7 +17,6 @@ from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property from reversion import revisions as reversion -from reversion.models import Version from versioning.versioning import RevisionMixin @@ -34,9 +33,13 @@ def filter_by_pk(filt, query): class Profile(AbstractUser): initials = models.CharField(max_length=5, null=True, blank=False) - phone = models.CharField(max_length=13, blank=True, default='') - api_key = models.CharField(max_length=40, blank=True, editable=False, default='') - is_approved = models.BooleanField(default=False, verbose_name="Approval Status", help_text="Designates whether a staff member has approved this user.") + phone = models.CharField(max_length=13, blank=True, default="") + api_key = models.CharField(max_length=40, blank=True, editable=False, default="") + is_approved = models.BooleanField( + default=False, + verbose_name="Approval Status", + help_text="Designates whether a staff member has approved this user.", + ) # Currently only populated by the admin approval email. TODO: Populate it each time we send any email, might need that... last_emailed = models.DateTimeField(blank=True, null=True) dark_theme = models.BooleanField(default=False) @@ -48,15 +51,18 @@ class Profile(AbstractUser): def make_api_key(cls): size = 20 chars = string.ascii_letters + string.digits - new_api_key = ''.join(random.choice(chars) for x in range(size)) + new_api_key = "".join(random.choice(chars) for x in range(size)) return new_api_key @property def profile_picture(self): url = "" if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None: - url = "https://www.gravatar.com/avatar/" + hashlib.md5( - self.email.encode('utf-8')).hexdigest() + "?d=wavatar&s=500" + url = ( + "https://www.gravatar.com/avatar/" + + hashlib.md5(self.email.encode("utf-8")).hexdigest() + + "?d=wavatar&s=500" + ) return url @property @@ -68,7 +74,11 @@ class Profile(AbstractUser): @property def latest_events(self): - return self.event_mic.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic', 'riskassessment', 'invoice').prefetch_related('checklists') + return ( + self.event_mic.order_by("-start_date") + .select_related("person", "organisation", "venue", "mic", "riskassessment", "invoice") + .prefetch_related("checklists") + ) @classmethod def admins(cls): @@ -77,22 +87,30 @@ class Profile(AbstractUser): @classmethod def users_awaiting_approval_count(cls): # last_login = None ensures we only pick up genuinely new users, not those that have been deactivated for inactivity - return Profile.objects.filter(is_approved=False, last_login=None, date_joined_date=timezone.now().date()).count() + return Profile.objects.filter( + is_approved=False, last_login=None, date_joined_date=timezone.now().date() + ).count() def __str__(self): return self.name def current_event(self): q = EventCheckIn.objects.filter(person=self, end_time=None) - return q.latest('time') if q.exists() else None + return q.latest("time") if q.exists() else None class ContactableManager(models.Manager): def search(self, query=None): qs = self.get_queryset() if query is not None: - or_lookup = Q(name__icontains=query) | Q(email__icontains=query) | Q(address__icontains=query) | Q(notes__icontains=query) | Q( - phone__startswith=query) | Q(phone__endswith=query) + or_lookup = ( + Q(name__icontains=query) + | Q(email__icontains=query) + | Q(address__icontains=query) + | Q(notes__icontains=query) + | Q(phone__startswith=query) + | Q(phone__endswith=query) + ) or_lookup = filter_by_pk(or_lookup, query) @@ -102,10 +120,10 @@ class ContactableManager(models.Manager): class Person(models.Model, RevisionMixin): name = models.CharField(max_length=50) - phone = models.CharField(max_length=15, blank=True, default='') - email = models.EmailField(blank=True, default='') - address = models.TextField(blank=True, default='') - notes = models.TextField(blank=True, default='') + phone = models.CharField(max_length=15, blank=True, default="") + email = models.EmailField(blank=True, default="") + address = models.TextField(blank=True, default="") + notes = models.TextField(blank=True, default="") objects = ContactableManager() @@ -119,7 +137,7 @@ class Person(models.Model, RevisionMixin): @property def organisations(self): o = [] - for e in Event.objects.filter(person=self).select_related('organisation'): + for e in Event.objects.filter(person=self).select_related("organisation"): if e.organisation: o.append(e.organisation) @@ -130,18 +148,18 @@ class Person(models.Model, RevisionMixin): @property def latest_events(self): - return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') + return self.event_set.order_by("-start_date").select_related("person", "organisation", "venue", "mic") def get_absolute_url(self): - return reverse('person_detail', kwargs={'pk': self.pk}) + return reverse("person_detail", kwargs={"pk": self.pk}) class Organisation(models.Model, RevisionMixin): name = models.CharField(max_length=50) - phone = models.CharField(max_length=15, blank=True, default='') - email = models.EmailField(blank=True, default='') - address = models.TextField(blank=True, default='') - notes = models.TextField(blank=True, default='') + phone = models.CharField(max_length=15, blank=True, default="") + email = models.EmailField(blank=True, default="") + address = models.TextField(blank=True, default="") + notes = models.TextField(blank=True, default="") union_account = models.BooleanField(default=False) objects = ContactableManager() @@ -156,7 +174,7 @@ class Organisation(models.Model, RevisionMixin): @property def persons(self): p = [] - for e in Event.objects.filter(organisation=self).select_related('person'): + for e in Event.objects.filter(organisation=self).select_related("person"): if e.person: p.append(e.person) @@ -167,10 +185,10 @@ class Organisation(models.Model, RevisionMixin): @property def latest_events(self): - return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') + return self.event_set.order_by("-start_date").select_related("person", "organisation", "venue", "mic") def get_absolute_url(self): - return reverse('organisation_detail', kwargs={'pk': self.pk}) + return reverse("organisation_detail", kwargs={"pk": self.pk}) class VatManager(models.Manager): @@ -201,8 +219,8 @@ class VatRate(models.Model, RevisionMixin): return self.rate * 100 class Meta: - ordering = ['-start_at'] - get_latest_by = 'start_at' + ordering = ["-start_at"] + get_latest_by = "start_at" def __str__(self): return f"{self.comment} {self.start_at} @ {self.as_percent}%" @@ -210,11 +228,11 @@ class VatRate(models.Model, RevisionMixin): class Venue(models.Model, RevisionMixin): name = models.CharField(max_length=255) - phone = models.CharField(max_length=15, blank=True, default='') - email = models.EmailField(blank=True, default='') + phone = models.CharField(max_length=15, blank=True, default="") + email = models.EmailField(blank=True, default="") three_phase_available = models.BooleanField(default=False) - notes = models.TextField(blank=True, default='') - address = models.TextField(blank=True, default='') + notes = models.TextField(blank=True, default="") + address = models.TextField(blank=True, default="") objects = ContactableManager() @@ -226,68 +244,85 @@ class Venue(models.Model, RevisionMixin): @property def latest_events(self): - return self.event_set.order_by('-start_date').select_related('person', 'organisation', 'venue', 'mic') + return self.event_set.order_by("-start_date").select_related("person", "organisation", "venue", "mic") def get_absolute_url(self): - return reverse('venue_detail', kwargs={'pk': self.pk}) + return reverse("venue_detail", kwargs={"pk": self.pk}) class EventManager(models.Manager): def current_events(self): - events = self.filter( - (models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) & ~models.Q( - status=Event.CANCELLED)) | # Starts after with no end - (models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q( - status=Event.CANCELLED)) | # Ends after - (models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q( - status=Event.CANCELLED)) | # Active dry hire - (models.Q(dry_hire=True, checked_in_by__isnull=True) & ( - models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT - models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started - ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') + events = ( + self.filter( + ( + models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False) + & ~models.Q(status=Event.CANCELLED) + ) # Starts after with no end + | ( + models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(status=Event.CANCELLED) + ) # Ends after + | ( + models.Q(dry_hire=True, start_date__gte=timezone.now()) & ~models.Q(status=Event.CANCELLED) + ) # Active dry hire + | ( + models.Q(dry_hire=True, checked_in_by__isnull=True) + & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED)) + ) # Active dry hire GT + | models.Q(status=Event.CANCELLED, start_date__gte=timezone.now()) # Canceled but not started + ) + .order_by("start_date", "end_date", "start_time", "end_time", "meet_at") + .select_related("person", "organisation", "venue", "mic") + ) return events def events_in_bounds(self, start, end): - events = self.filter( - (models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds - (models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds - (models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds - (models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds - - (models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after - (models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after - (models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after - (models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after - (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after - - ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', - 'organisation', - 'venue', 'mic') + events = ( + self.filter( + (models.Q(start_date__gte=start.date(), start_date__lte=end.date())) # Start date in bounds + | (models.Q(end_date__gte=start.date(), end_date__lte=end.date())) # End date in bounds + | (models.Q(access_at__gte=start, access_at__lte=end)) # Access at in bounds + | (models.Q(meet_at__gte=start, meet_at__lte=end)) # Meet at in bounds + | (models.Q(start_date__lte=start, end_date__gte=end)) # Start before, end after + | (models.Q(access_at__lte=start, start_date__gte=end)) # Access before, start after + | (models.Q(access_at__lte=start, end_date__gte=end)) # Access before, end after + | (models.Q(meet_at__lte=start, start_date__gte=end)) # Meet before, start after + | (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after + ) + .order_by("start_date", "end_date", "start_time", "end_time", "meet_at") + .select_related("person", "organisation", "venue", "mic") + ) return events def rig_count(self): event_count = self.filter( - (models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False, - is_rig=True) & ~models.Q( - status=Event.CANCELLED)) | # Starts after with no end - (models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q( - status=Event.CANCELLED)) | # Ends after - (models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q( - status=Event.CANCELLED)) # Active dry hire + ( + models.Q(start_date__gte=timezone.now(), end_date__isnull=True, dry_hire=False, is_rig=True) + & ~models.Q(status=Event.CANCELLED) + ) # Starts after with no end + | ( + models.Q(end_date__gte=timezone.now(), dry_hire=False, is_rig=True) & ~models.Q(status=Event.CANCELLED) + ) # Ends after + | ( + models.Q(dry_hire=True, start_date__gte=timezone.now(), is_rig=True) & ~models.Q(status=Event.CANCELLED) + ) # Active dry hire ).count() return event_count def waiting_invoices(self): - events = self.filter( - ( - models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end - models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before - ) & models.Q(invoice__isnull=True) & # Has not already been invoiced - models.Q(is_rig=True) # Is a rig (not non-rig) - ).order_by('start_date') \ - .select_related('person', 'organisation', 'venue', 'mic') \ - .prefetch_related('items') + events = ( + self.filter( + ( + models.Q(start_date__lte=datetime.date.today(), end_date__isnull=True) # Starts before with no end + | models.Q(end_date__lte=datetime.date.today()) # Or has end date, finishes before + ) + & models.Q(invoice__isnull=True) # Has not already been invoiced + & models.Q(is_rig=True) # Is a rig (not non-rig) + ) + .order_by("start_date") + .select_related("person", "organisation", "venue", "mic") + .prefetch_related("items") + ) return events @@ -313,11 +348,11 @@ def validate_forum_url(value): if not value: return # Required error is done the field obj = urlparse(value) - if obj.hostname not in ('forum.nottinghamtec.co.uk'): - raise ValidationError('URL must point to a location on the TEC Forum') + if obj.hostname not in ("forum.nottinghamtec.co.uk"): + raise ValidationError("URL must point to a location on the TEC Forum") -@reversion.register(follow=['items']) +@reversion.register(follow=["items"]) class Event(models.Model, RevisionMixin): # Done to make it much nicer on the database PROVISIONAL = 0 @@ -325,23 +360,24 @@ class Event(models.Model, RevisionMixin): BOOKED = 2 CANCELLED = 3 EVENT_STATUS_CHOICES = ( - (PROVISIONAL, 'Provisional'), - (CONFIRMED, 'Confirmed'), - (BOOKED, 'Booked'), - (CANCELLED, 'Cancelled'), + (PROVISIONAL, "Provisional"), + (CONFIRMED, "Confirmed"), + (BOOKED, "Booked"), + (CANCELLED, "Cancelled"), ) name = models.CharField(max_length=255) - person = models.ForeignKey('Person', null=True, blank=True, on_delete=models.CASCADE) - organisation = models.ForeignKey('Organisation', blank=True, null=True, on_delete=models.CASCADE) - venue = models.ForeignKey('Venue', blank=True, null=True, on_delete=models.CASCADE) - description = models.TextField(blank=True, default='') - notes = models.TextField(blank=True, default='') + person = models.ForeignKey("Person", null=True, blank=True, on_delete=models.CASCADE) + organisation = models.ForeignKey("Organisation", blank=True, null=True, on_delete=models.CASCADE) + venue = models.ForeignKey("Venue", blank=True, null=True, on_delete=models.CASCADE) + description = models.TextField(blank=True, default="") + notes = models.TextField(blank=True, default="") status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL) dry_hire = models.BooleanField(default=False) is_rig = models.BooleanField(default=True) - based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True, - null=True) + based_on = models.ForeignKey( + "Event", on_delete=models.SET_NULL, related_name="future_events", blank=True, null=True + ) # Timing start_date = models.DateField() @@ -355,21 +391,28 @@ class Event(models.Model, RevisionMixin): parking_and_access = models.BooleanField(default=False) # Crew management - checked_in_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_checked_in', blank=True, null=True, - on_delete=models.CASCADE) - mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='event_mic', blank=True, null=True, - verbose_name="MIC", on_delete=models.CASCADE) + checked_in_by = models.ForeignKey( + settings.AUTH_USER_MODEL, related_name="event_checked_in", blank=True, null=True, on_delete=models.CASCADE + ) + mic = models.ForeignKey( + settings.AUTH_USER_MODEL, + related_name="event_mic", + blank=True, + null=True, + verbose_name="MIC", + on_delete=models.CASCADE, + ) # Monies - purchase_order = models.CharField(max_length=255, blank=True, default='', verbose_name='PO') - collector = models.CharField(max_length=255, blank=True, default='', verbose_name='collected by') + purchase_order = models.CharField(max_length=255, blank=True, default="", verbose_name="PO") + collector = models.CharField(max_length=255, blank=True, default="", verbose_name="collected by") # Authorisation request details - auth_request_by = models.ForeignKey('Profile', null=True, blank=True, on_delete=models.CASCADE) + auth_request_by = models.ForeignKey("Profile", null=True, blank=True, on_delete=models.CASCADE) auth_request_at = models.DateTimeField(null=True, blank=True) - auth_request_to = models.EmailField(blank=True, default='') + auth_request_to = models.EmailField(blank=True, default="") - forum_url = models.URLField(default='', blank=True, validators=[validate_forum_url]) + forum_url = models.URLField(default="", blank=True, validators=[validate_forum_url]) @property def display_id(self): @@ -391,9 +434,11 @@ class Event(models.Model, RevisionMixin): @property def sum_total(self): total = self.items.aggregate( - sum_total=models.Sum(models.F('cost') * models.F('quantity'), - output_field=models.DecimalField(max_digits=10, decimal_places=2)) - )['sum_total'] + sum_total=models.Sum( + models.F("cost") * models.F("quantity"), + output_field=models.DecimalField(max_digits=10, decimal_places=2), + ) + )["sum_total"] if total: return total return Decimal("0.00") @@ -407,7 +452,7 @@ class Event(models.Model, RevisionMixin): # No VAT is owed on internal transfers if self.internal: return 0 - return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal('.01')) + return Decimal(self.sum_total * self.vat_rate.rate).quantize(Decimal(".01")) """ Inc VAT @@ -415,15 +460,15 @@ class Event(models.Model, RevisionMixin): @property def total(self): - return Decimal(self.sum_total + self.vat).quantize(Decimal('.01')) + return Decimal(self.sum_total + self.vat).quantize(Decimal(".01")) @property def cancelled(self): - return (self.status == self.CANCELLED) + return self.status == self.CANCELLED @property def confirmed(self): - return (self.status == self.BOOKED or self.status == self.CONFIRMED) + return self.status == self.BOOKED or self.status == self.CONFIRMED @property def hs_done(self): @@ -520,7 +565,7 @@ class Event(models.Model, RevisionMixin): objects = EventManager() def get_absolute_url(self): - return reverse('event_detail', kwargs={'pk': self.pk}) + return reverse("event_detail", kwargs={"pk": self.pk}) def __str__(self): return f"{self.display_id} | {self.name}" @@ -528,18 +573,26 @@ class Event(models.Model, RevisionMixin): def clean(self): errdict = {} if self.end_date and self.start_date > self.end_date: - errdict['end_date'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.'] + errdict["end_date"] = ["Unless you've invented time travel, the event can't finish before it has started."] startEndSameDay = not self.end_date or self.end_date == self.start_date hasStartAndEnd = self.has_start_time and self.has_end_time if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time: - errdict['end_time'] = ['Unless you\'ve invented time travel, the event can\'t finish before it has started.'] + errdict["end_time"] = ["Unless you've invented time travel, the event can't finish before it has started."] if self.access_at is not None: if self.access_at.date() > self.start_date: - errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.'] - elif self.start_time is not None and self.start_date == self.access_at.date() and self.access_at.time() > self.start_time: - errdict['access_at'] = ['Regardless of what some clients might think, access time cannot be after the event has started.'] + errdict["access_at"] = [ + "Regardless of what some clients might think, access time cannot be after the event has started." + ] + elif ( + self.start_time is not None + and self.start_date == self.access_at.date() + and self.access_at.time() > self.start_time + ): + errdict["access_at"] = [ + "Regardless of what some clients might think, access time cannot be after the event has started." + ] if errdict != {}: # If there was an error when validation raise ValidationError(errdict) @@ -552,9 +605,9 @@ class Event(models.Model, RevisionMixin): @reversion.register class EventItem(models.Model, RevisionMixin): - event = models.ForeignKey('Event', related_name='items', blank=True, on_delete=models.CASCADE) + event = models.ForeignKey("Event", related_name="items", blank=True, on_delete=models.CASCADE) name = models.CharField(max_length=255) - description = models.TextField(blank=True, default='') + description = models.TextField(blank=True, default="") quantity = models.IntegerField() cost = models.DecimalField(max_digits=10, decimal_places=2) order = models.IntegerField() @@ -566,7 +619,7 @@ class EventItem(models.Model, RevisionMixin): return self.cost * self.quantity class Meta: - ordering = ['order'] + ordering = ["order"] def __str__(self): return f"{self.event_id}.{self.order}: {self.event.name} | {self.name}" @@ -578,16 +631,16 @@ class EventItem(models.Model, RevisionMixin): @reversion.register class EventAuthorisation(models.Model, RevisionMixin): - event = models.OneToOneField('Event', related_name='authorisation', on_delete=models.CASCADE) + event = models.OneToOneField("Event", related_name="authorisation", on_delete=models.CASCADE) email = models.EmailField() name = models.CharField(max_length=255) - uni_id = models.CharField(max_length=10, blank=True, default='', verbose_name="University ID") - account_code = models.CharField(max_length=50, default='', blank=True) + uni_id = models.CharField(max_length=10, blank=True, default="", verbose_name="University ID") + account_code = models.CharField(max_length=50, default="", blank=True) amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="authorisation amount") - sent_by = models.ForeignKey('Profile', on_delete=models.CASCADE) + sent_by = models.ForeignKey("Profile", on_delete=models.CASCADE) def get_absolute_url(self): - return reverse('event_detail', kwargs={'pk': self.event_id}) + return reverse("event_detail", kwargs={"pk": self.event_id}) @property def activity_feed_string(self): @@ -597,15 +650,17 @@ class EventAuthorisation(models.Model, RevisionMixin): class InvoiceManager(models.Manager): def outstanding_invoices(self): # Manual query is the only way I have found to do this efficiently. Not ideal but needs must - sql = "SELECT * FROM " \ - "(SELECT " \ - "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \ - "(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \ - "(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \ - "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \ - "AS sub " \ - "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \ - "ORDER BY invoice_date" + sql = ( + "SELECT * FROM " + "(SELECT " + '(SELECT COUNT(p.amount) FROM "RIGS_payment" AS p WHERE p.invoice_id="RIGS_invoice".id) AS "payment_count", ' + '(SELECT SUM(ei.cost * ei.quantity) FROM "RIGS_eventitem" AS ei WHERE ei.event_id="RIGS_invoice".event_id) AS "cost", ' + '(SELECT SUM(p.amount) FROM "RIGS_payment" AS p WHERE p.invoice_id="RIGS_invoice".id) AS "payments", ' + '"RIGS_invoice"."id", "RIGS_invoice"."event_id", "RIGS_invoice"."invoice_date", "RIGS_invoice"."void" FROM "RIGS_invoice") ' + "AS sub " + "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" + "ORDER BY invoice_date" + ) query = self.raw(sql) return query @@ -639,13 +694,13 @@ class InvoiceManager(models.Manager): return qs -@reversion.register(follow=['payment_set']) +@reversion.register(follow=["payment_set"]) class Invoice(models.Model, RevisionMixin): - event = models.OneToOneField('Event', on_delete=models.CASCADE) + event = models.OneToOneField("Event", on_delete=models.CASCADE) invoice_date = models.DateField(auto_now_add=True) void = models.BooleanField(default=False) - reversion_perm = 'RIGS.view_invoice' + reversion_perm = "RIGS.view_invoice" objects = InvoiceManager() @@ -659,7 +714,7 @@ class Invoice(models.Model, RevisionMixin): @property def payment_total(self): - total = self.payment_set.aggregate(total=models.Sum('amount'))['total'] + total = self.payment_set.aggregate(total=models.Sum("amount"))["total"] if total: return total return Decimal("0.00") @@ -673,7 +728,7 @@ class Invoice(models.Model, RevisionMixin): return self.balance == 0 or self.void def get_absolute_url(self): - return reverse('invoice_detail', kwargs={'pk': self.pk}) + return reverse("invoice_detail", kwargs={"pk": self.pk}) @property def activity_feed_string(self): @@ -687,26 +742,26 @@ class Invoice(models.Model, RevisionMixin): return f"#{self.pk:05d}" class Meta: - ordering = ['-invoice_date'] + ordering = ["-invoice_date"] @reversion.register class Payment(models.Model, RevisionMixin): - CASH = 'C' - INTERNAL = 'I' - EXTERNAL = 'E' - ADJUSTMENT = 'T' + CASH = "C" + INTERNAL = "I" + EXTERNAL = "E" + ADJUSTMENT = "T" METHODS = ( - (CASH, 'Cash'), - (INTERNAL, 'Internal'), - (EXTERNAL, 'External'), - (ADJUSTMENT, 'TEC Adjustment'), + (CASH, "Cash"), + (INTERNAL, "Internal"), + (EXTERNAL, "External"), + (ADJUSTMENT, "TEC Adjustment"), ) - invoice = models.ForeignKey('Invoice', on_delete=models.CASCADE) + invoice = models.ForeignKey("Invoice", on_delete=models.CASCADE) date = models.DateField() - amount = models.DecimalField(max_digits=10, decimal_places=2, help_text='Please use ex. VAT') - method = models.CharField(max_length=2, choices=METHODS, default='', blank=True) + amount = models.DecimalField(max_digits=10, decimal_places=2, help_text="Please use ex. VAT") + method = models.CharField(max_length=2, choices=METHODS, default="", blank=True) reversion_hide = True @@ -722,98 +777,164 @@ def validate_url(value): if not value: return # Required error is done the field obj = urlparse(value) - if obj.hostname not in ('nottinghamtec.sharepoint.com'): - raise ValidationError('URL must point to a location on the TEC Sharepoint') + if obj.hostname not in ("nottinghamtec.sharepoint.com"): + raise ValidationError("URL must point to a location on the TEC Sharepoint") class ReviewableModel(models.Model): reviewed_at = models.DateTimeField(null=True, blank=True) - reviewed_by = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, - verbose_name="Reviewer", on_delete=models.CASCADE) + reviewed_by = models.ForeignKey( + settings.AUTH_USER_MODEL, blank=True, null=True, verbose_name="Reviewer", on_delete=models.CASCADE + ) class Meta: abstract = True @cached_property def fieldz(self): - return [n.name for n in list(self._meta.get_fields()) if n.name != 'reviewed_at' and n.name != 'reviewed_by' and not n.is_relation and not n.auto_created] + return [ + n.name + for n in list(self._meta.get_fields()) + if n.name != "reviewed_at" and n.name != "reviewed_by" and not n.is_relation and not n.auto_created + ] @reversion.register class RiskAssessment(ReviewableModel, RevisionMixin): - SMALL = (0, 'Small') - MEDIUM = (1, 'Medium') - LARGE = (2, 'Large') + SMALL = (0, "Small") + MEDIUM = (1, "Medium") + LARGE = (2, "Large") SIZES = (SMALL, MEDIUM, LARGE) - event = models.OneToOneField('Event', on_delete=models.CASCADE) + event = models.OneToOneField("Event", on_delete=models.CASCADE) # General - nonstandard_equipment = models.BooleanField(help_text="Does the event require any hired in equipment or use of equipment that is not covered by " - "TEC's standard risk assessments and method statements?") - nonstandard_use = models.BooleanField(help_text="Are TEC using their equipment in a way that is abnormal?
    i.e. Not covered by TECs standard health and safety documentation") - contractors = models.BooleanField(help_text="Are you using any external contractors?
    i.e. Freelancers/Crewing Companies") - other_companies = models.BooleanField(help_text="Are TEC working with any other companies on site?
    e.g. TEC is providing the lighting while another company does sound") + nonstandard_equipment = models.BooleanField( + help_text="Does the event require any hired in equipment or use of equipment that is not covered by " + "TEC's standard risk assessments and method statements?" + ) + nonstandard_use = models.BooleanField( + help_text="Are TEC using their equipment in a way that is abnormal?
    i.e. Not covered by TECs standard health and safety documentation" + ) + contractors = models.BooleanField( + help_text="Are you using any external contractors?
    i.e. Freelancers/Crewing Companies" + ) + other_companies = models.BooleanField( + help_text="Are TEC working with any other companies on site?
    e.g. TEC is providing the lighting while another company does sound" + ) crew_fatigue = models.BooleanField(help_text="Is crew fatigue likely to be a risk at any point during this event?") - general_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?") + general_notes = models.TextField( + blank=True, + default="", + help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?", + ) # Power - big_power = models.BooleanField(help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?") - power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='power_mic', blank=True, null=True, - verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC? (if yes to the above question, this person must be a Power Technician or Power Supervisor)") + big_power = models.BooleanField( + help_text="Does the event require larger power supplies than 13A or 16A single phase wall sockets, or draw more than 20A total current?" + ) + power_mic = models.ForeignKey( + settings.AUTH_USER_MODEL, + related_name="power_mic", + blank=True, + null=True, + verbose_name="Power MIC", + on_delete=models.CASCADE, + help_text="Who is the Power MIC? (if yes to the above question, this person must be a Power Technician or Power Supervisor)", + ) outside = models.BooleanField(help_text="Is the event outdoors?") generators = models.BooleanField(help_text="Will generators be used?") other_companies_power = models.BooleanField(help_text="Will TEC be supplying power to any other companies?") - nonstandard_equipment_power = models.BooleanField(help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?") - multiple_electrical_environments = models.BooleanField(help_text="Will the electrical installation occupy more than one electrical environment?") - power_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?") - power_plan = models.URLField(blank=True, default='', help_text="Upload your power plan to the Sharepoint and submit a link", validators=[validate_url]) + nonstandard_equipment_power = models.BooleanField( + help_text="Does the power plan require the use of any power equipment (distros, dimmers, motor controllers, etc.) that does not belong to TEC?" + ) + multiple_electrical_environments = models.BooleanField( + help_text="Will the electrical installation occupy more than one electrical environment?" + ) + power_notes = models.TextField( + blank=True, + default="", + help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?", + ) + power_plan = models.URLField( + blank=True, + default="", + help_text="Upload your power plan to the Sharepoint and submit a link", + validators=[validate_url], + ) # Sound - noise_monitoring = models.BooleanField(help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?") - sound_notes = models.TextField(blank=True, default='', help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?") + noise_monitoring = models.BooleanField( + help_text="Does the event require noise monitoring or any non-standard procedures in order to comply with health and safety legislation or site rules?" + ) + sound_notes = models.TextField( + blank=True, + default="", + help_text="Did you have to consult a supervisor about any of the above? If so who did you consult and what was the outcome?", + ) # Site known_venue = models.BooleanField(help_text="Is this venue new to you (the MIC) or new to TEC?") - safe_loading = models.BooleanField(help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)") + safe_loading = models.BooleanField( + help_text="Are there any issues preventing a safe load in or out? (e.g. sufficient lighting, flat, not in a crowded area etc.)" + ) safe_storage = models.BooleanField(help_text="Are there any problems with safe and secure equipment storage?") - area_outside_of_control = models.BooleanField(help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?") - barrier_required = models.BooleanField(help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?") - nonstandard_emergency_procedure = models.BooleanField(help_text="Does the emergency procedure for the event differ from TEC's standard procedures?") + area_outside_of_control = models.BooleanField( + help_text="Is any part of the work area out of TEC's direct control or openly accessible during the build or breakdown period?" + ) + barrier_required = models.BooleanField( + help_text="Is there a requirement for TEC to provide any barrier for security or protection of persons/equipment?" + ) + nonstandard_emergency_procedure = models.BooleanField( + help_text="Does the emergency procedure for the event differ from TEC's standard procedures?" + ) # Structures - special_structures = models.BooleanField(help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?") - suspended_structures = models.BooleanField(help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?") - persons_responsible_structures = models.TextField(blank=True, default='', help_text="Who are the persons on site responsible for their use?") - rigging_plan = models.URLField(blank=True, default='', help_text="Upload your rigging plan to the Sharepoint and submit a link", validators=[validate_url]) + special_structures = models.BooleanField( + help_text="Does the event require use of winch stands, motors, MPT Towers, or staging?" + ) + suspended_structures = models.BooleanField( + help_text="Are any structures (excluding projector screens and IWBs) being suspended from TEC's structures?" + ) + persons_responsible_structures = models.TextField( + blank=True, default="", help_text="Who are the persons on site responsible for their use?" + ) + rigging_plan = models.URLField( + blank=True, + default="", + help_text="Upload your rigging plan to the Sharepoint and submit a link", + validators=[validate_url], + ) # Venue Access - parking_and_access = models.BooleanField(help_text="Are there additional requirements for parking and access to the venue? (i.e. campus parking permits, event access wristbands)") + parking_and_access = models.BooleanField( + help_text="Are there additional requirements for parking and access to the venue? (i.e. campus parking permits, event access wristbands)" + ) # Blimey that was a lot of options supervisor_consulted = models.BooleanField(null=True) expected_values = { - 'nonstandard_equipment': False, - 'nonstandard_use': False, - 'contractors': False, - 'other_companies': False, - 'crew_fatigue': False, + "nonstandard_equipment": False, + "nonstandard_use": False, + "contractors": False, + "other_companies": False, + "crew_fatigue": False, # 'big_power': False Doesn't require checking with a super either way - 'generators': False, - 'other_companies_power': False, - 'nonstandard_equipment_power': False, - 'multiple_electrical_environments': False, - 'noise_monitoring': False, - 'known_venue': False, - 'safe_loading': False, - 'safe_storage': False, - 'area_outside_of_control': False, - 'barrier_required': False, - 'nonstandard_emergency_procedure': False, - 'special_structures': False, - 'suspended_structures': False, - 'parking_and_access': False + "generators": False, + "other_companies_power": False, + "nonstandard_equipment_power": False, + "multiple_electrical_environments": False, + "noise_monitoring": False, + "known_venue": False, + "safe_loading": False, + "safe_storage": False, + "area_outside_of_control": False, + "barrier_required": False, + "nonstandard_emergency_procedure": False, + "special_structures": False, + "suspended_structures": False, + "parking_and_access": False, } inverted_fields = {key: value for (key, value) in expected_values.items() if not value}.keys() @@ -823,15 +944,18 @@ class RiskAssessment(ReviewableModel, RevisionMixin): raise forms.ValidationError("Engage brain, please. No generators indoors!(!)") class Meta: - ordering = ['event'] - permissions = [ - ('review_riskassessment', 'Can review Risk Assessments') - ] + ordering = ["event"] + permissions = [("review_riskassessment", "Can review Risk Assessments")] @property def event_size(self): # Confirm event size. Check all except generators, since generators entails outside - if self.outside or self.other_companies_power or self.nonstandard_equipment_power or self.multiple_electrical_environments: + if ( + self.outside + or self.other_companies_power + or self.nonstandard_equipment_power + or self.multiple_electrical_environments + ): return self.LARGE[0] elif self.big_power: return self.MEDIUM[0] @@ -845,7 +969,7 @@ class RiskAssessment(ReviewableModel, RevisionMixin): return f"{self.pk} | {self.event}" def get_absolute_url(self): - return reverse('ra_detail', kwargs={'pk': self.pk}) + return reverse("ra_detail", kwargs={"pk": self.pk}) @property def activity_feed_string(self): @@ -858,29 +982,37 @@ class RiskAssessment(ReviewableModel, RevisionMixin): @reversion.register class EventChecklist(ReviewableModel, RevisionMixin): - event = models.ForeignKey('Event', related_name='checklists', on_delete=models.CASCADE) + event = models.ForeignKey("Event", related_name="checklists", on_delete=models.CASCADE) # General - venue = models.ForeignKey('Venue', on_delete=models.CASCADE) + venue = models.ForeignKey("Venue", on_delete=models.CASCADE) date = models.DateField() # Safety Checks - safe_parking = models.BooleanField(blank=True, null=True, help_text="Vehicles parked safely?
    (does not obstruct venue access)") - safe_packing = models.BooleanField(blank=True, null=True, help_text="Equipment packed away safely?
    (including flightcases)") + safe_parking = models.BooleanField( + blank=True, null=True, help_text="Vehicles parked safely?
    (does not obstruct venue access)" + ) + safe_packing = models.BooleanField( + blank=True, null=True, help_text="Equipment packed away safely?
    (including flightcases)" + ) exits = models.BooleanField(blank=True, null=True, help_text="Emergency exits clear?") - trip_hazard = models.BooleanField(blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?") - warning_signs = models.BooleanField(blank=True, help_text="Warning signs in place?
    (strobe, smoke, power etc.)") + trip_hazard = models.BooleanField( + blank=True, null=True, help_text="Appropriate barriers around kit and cabling secured?" + ) + warning_signs = models.BooleanField( + blank=True, help_text="Warning signs in place?
    (strobe, smoke, power etc.)" + ) ear_plugs = models.BooleanField(blank=True, null=True, help_text="Ear plugs issued to crew where needed?") - hs_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of Safety Bag/Box") - extinguishers_location = models.CharField(blank=True, default='', max_length=255, help_text="Location of fire extinguishers") + hs_location = models.CharField(blank=True, default="", max_length=255, help_text="Location of Safety Bag/Box") + extinguishers_location = models.CharField( + blank=True, default="", max_length=255, help_text="Location of fire extinguishers" + ) inverted_fields = [] class Meta: - ordering = ['event'] - permissions = [ - ('review_eventchecklist', 'Can review Event Checklists') - ] + ordering = ["event"] + permissions = [("review_eventchecklist", "Can review Event Checklists")] def __str__(self): return f"{self.pk} - {self.event}" @@ -890,7 +1022,7 @@ class EventChecklist(ReviewableModel, RevisionMixin): return str(self.event) def get_absolute_url(self): - return reverse('ec_detail', kwargs={'pk': self.pk}) + return reverse("ec_detail", kwargs={"pk": self.pk}) @reversion.register @@ -898,57 +1030,114 @@ class PowerTestRecord(ReviewableModel, RevisionMixin): earth_fault_text = "Earth Fault Loop Impedance (ZS) / Ω" pssc_text = "Prospective Short Circuit Current / A" - event = models.ForeignKey('Event', related_name='power_tests', on_delete=models.CASCADE) - power_mic = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, related_name='checklists', - verbose_name="Power MIC", on_delete=models.CASCADE, help_text="Who is the Power MIC?") - venue = models.ForeignKey('Venue', on_delete=models.CASCADE) - notes = models.TextField(blank=True, default='') + event = models.ForeignKey("Event", related_name="power_tests", on_delete=models.CASCADE) + power_mic = models.ForeignKey( + settings.AUTH_USER_MODEL, + blank=True, + null=True, + related_name="checklists", + verbose_name="Power MIC", + on_delete=models.CASCADE, + help_text="Who is the Power MIC?", + ) + venue = models.ForeignKey("Venue", on_delete=models.CASCADE) + notes = models.TextField(blank=True, default="") # Small Electrical Checks rcds = models.BooleanField(blank=True, null=True, help_text="RCDs installed where needed and tested?") - supply_test = models.BooleanField(blank=True, null=True, help_text="Electrical supplies tested?
    (using socket tester)") + supply_test = models.BooleanField( + blank=True, null=True, help_text="Electrical supplies tested?
    (using socket tester)" + ) # Shared electrical checks - earthing = models.BooleanField(blank=True, null=True, help_text="Equipment appropriately earthed?
    (truss, stage, generators etc)") + earthing = models.BooleanField( + blank=True, + null=True, + help_text="Equipment appropriately earthed?
    (truss, stage, generators etc)", + ) pat = models.BooleanField(blank=True, null=True, help_text="All equipment in PAT period?") # Medium Electrical Checks - source_rcd = models.BooleanField(blank=True, null=True, help_text="Source RCD protected?
    (if cable is more than 3m long) ") - labelling = models.BooleanField(blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?") + source_rcd = models.BooleanField( + blank=True, null=True, help_text="Source RCD protected?
    (if cable is more than 3m long) " + ) + labelling = models.BooleanField( + blank=True, null=True, help_text="Appropriate and clear labelling on distribution and cabling?" + ) # First Distro - fd_voltage_l1 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N") - fd_voltage_l2 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N") - fd_voltage_l3 = models.IntegerField(blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N") - fd_phase_rotation = models.BooleanField(blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation
    (if required)") - fd_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text) + fd_voltage_l1 = models.IntegerField( + blank=True, null=True, verbose_name="First Distro Voltage L1-N", help_text="L1 - N" + ) + fd_voltage_l2 = models.IntegerField( + blank=True, null=True, verbose_name="First Distro Voltage L2-N", help_text="L2 - N" + ) + fd_voltage_l3 = models.IntegerField( + blank=True, null=True, verbose_name="First Distro Voltage L3-N", help_text="L3 - N" + ) + fd_phase_rotation = models.BooleanField( + blank=True, null=True, verbose_name="Phase Rotation", help_text="Phase Rotation
    (if required)" + ) + fd_earth_fault = models.DecimalField( + blank=True, + null=True, + max_digits=6, + decimal_places=2, + verbose_name="Earth Fault Loop Impedance", + help_text=earth_fault_text, + ) fd_pssc = models.IntegerField(blank=True, null=True, verbose_name="PSCC", help_text=pssc_text) # Worst case points - w1_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") + w1_description = models.CharField(blank=True, default="", max_length=255, help_text="Description") w1_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?") w1_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V") - w1_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text) - w2_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") + w1_earth_fault = models.DecimalField( + blank=True, + null=True, + max_digits=6, + decimal_places=2, + verbose_name="Earth Fault Loop Impedance", + help_text=earth_fault_text, + ) + w2_description = models.CharField(blank=True, default="", max_length=255, help_text="Description") w2_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?") w2_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V") - w2_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text) - w3_description = models.CharField(blank=True, default='', max_length=255, help_text="Description") + w2_earth_fault = models.DecimalField( + blank=True, + null=True, + max_digits=6, + decimal_places=2, + verbose_name="Earth Fault Loop Impedance", + help_text=earth_fault_text, + ) + w3_description = models.CharField(blank=True, default="", max_length=255, help_text="Description") w3_polarity = models.BooleanField(blank=True, null=True, help_text="Polarity Checked?") w3_voltage = models.IntegerField(blank=True, null=True, help_text="Voltage / V") - w3_earth_fault = models.DecimalField(blank=True, null=True, max_digits=6, decimal_places=2, verbose_name="Earth Fault Loop Impedance", help_text=earth_fault_text) + w3_earth_fault = models.DecimalField( + blank=True, + null=True, + max_digits=6, + decimal_places=2, + verbose_name="Earth Fault Loop Impedance", + help_text=earth_fault_text, + ) - all_rcds_tested = models.BooleanField(blank=True, null=True, help_text="All circuit RCDs tested?
    (using test button)") - public_sockets_tested = models.BooleanField(blank=True, null=True, help_text="Public/Performer accessible circuits tested?
    (using socket tester)") + all_rcds_tested = models.BooleanField( + blank=True, null=True, help_text="All circuit RCDs tested?
    (using test button)" + ) + public_sockets_tested = models.BooleanField( + blank=True, + null=True, + help_text="Public/Performer accessible circuits tested?
    (using socket tester)", + ) class Meta: - ordering = ['event'] - permissions = [ - ('review_power', 'Can review Power Test Records') - ] + ordering = ["event"] + permissions = [("review_power", "Can review Power Test Records")] def __str__(self): return f"{self.pk} - {self.event}" def get_absolute_url(self): - return reverse('pt_detail', kwargs={'pk': self.pk}) + return reverse("pt_detail", kwargs={"pk": self.pk}) @property def activity_feed_string(self): @@ -960,8 +1149,8 @@ class PowerTestRecord(ReviewableModel, RevisionMixin): class EventCheckIn(models.Model): - event = models.ForeignKey('Event', related_name='crew', on_delete=models.CASCADE) - person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='checkins', on_delete=models.CASCADE) + event = models.ForeignKey("Event", related_name="crew", on_delete=models.CASCADE) + person = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="checkins", on_delete=models.CASCADE) time = models.DateTimeField() role = models.CharField(max_length=50, blank=True) vehicle = models.CharField(max_length=100, blank=True) @@ -978,7 +1167,7 @@ class EventCheckIn(models.Model): raise ValidationError("May not check out before you've checked in." + sass) def get_absolute_url(self): - return reverse('event_detail', kwargs={'pk': self.event_id}) + return reverse("event_detail", kwargs={"pk": self.event_id}) def active(self): - return end_time is not None + return self.end_time is None diff --git a/RIGS/signals.py b/RIGS/signals.py index b17e751c..5c9b24c7 100644 --- a/RIGS/signals.py +++ b/RIGS/signals.py @@ -3,9 +3,8 @@ import urllib.error import urllib.parse import urllib.request from io import BytesIO -import datetime -from PyPDF2 import PdfFileReader, PdfFileMerger +from pypdf import PdfReader, PdfWriter from django.conf import settings from django.contrib.staticfiles import finders from django.core.cache import cache @@ -25,18 +24,18 @@ from RIGS import models def send_eventauthorisation_success_email(instance): # Generate PDF first to prevent context conflicts context = { - 'object': instance.event, - 'receipt': True, - 'current_user': False, + "object": instance.event, + "receipt": True, + "current_user": False, } - template = get_template('event_print.xml') - merger = PdfFileMerger() + template = get_template("event_print.xml") + merger = PdfWriter() rml = template.render(context) buffer = rml2pdf.parseString(rml) - merger.append(PdfFileReader(buffer)) + merger.append(PdfReader(buffer)) buffer.close() terms = urllib.request.urlopen(settings.TERMS_OF_HIRE_URL) @@ -47,13 +46,13 @@ def send_eventauthorisation_success_email(instance): # Produce email content context = { - 'object': instance, + "object": instance, } if instance.event.person is not None and instance.email == instance.event.person.email: - context['to_name'] = instance.event.person.name + context["to_name"] = instance.event.person.name elif instance.event.organisation is not None and instance.email == instance.event.organisation.email: - context['to_name'] = instance.event.organisation.name + context["to_name"] = instance.event.organisation.name subject = f"{instance.event.display_id} | {instance.event.name} - Event Authorised" @@ -64,17 +63,19 @@ def send_eventauthorisation_success_email(instance): reply_to=[settings.AUTHORISATION_NOTIFICATION_ADDRESS], ) - css = finders.find('css/email.css') - html = Premailer(get_template("email/eventauthorisation_client_success.html").render(context), - external_styles=css).transform() - client_email.attach_alternative(html, 'text/html') + css = finders.find("css/email.css") + html = Premailer( + get_template("email/eventauthorisation_client_success.html").render(context), + external_styles=css, + allow_loading_external_files=True, + ).transform() + client_email.attach_alternative(html, "text/html") - escapedEventName = re.sub(r'[^a-zA-Z0-9 \n\.]', '', instance.event.name) + escapedEventName = re.sub(r"[^a-zA-Z0-9 \n\.]", "", instance.event.name) - client_email.attach(f'{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf', - merged.getvalue(), - 'application/pdf' - ) + client_email.attach( + f"{instance.event.display_id} - {escapedEventName} - CONFIRMATION.pdf", merged.getvalue(), "application/pdf" + ) if instance.event.mic: mic_email_address = instance.event.mic.email @@ -82,9 +83,7 @@ def send_eventauthorisation_success_email(instance): mic_email_address = settings.AUTHORISATION_NOTIFICATION_ADDRESS mic_email = EmailMessage( - subject, - get_template("email/eventauthorisation_mic_success.txt").render(context), - to=[mic_email_address] + subject, get_template("email/eventauthorisation_mic_success.txt").render(context), to=[mic_email_address] ) # Now we have both emails successfully generated, send them out @@ -110,10 +109,11 @@ def send_admin_awaiting_approval_email(user, request, **kwargs): # Check we've ever emailed them before and if so, if cooldown has passed. if admin.last_emailed is None or admin.last_emailed + settings.EMAIL_COOLDOWN <= timezone.now(): context = { - 'request': request, - 'link_suffix': reverse("admin:RIGS_profile_changelist") + f'?is_approved__exact=0&date_joined__date={timezone.now().date()}', - 'number_of_users': models.Profile.users_awaiting_approval_count(), - 'to_name': admin.first_name + "request": request, + "link_suffix": reverse("admin:RIGS_profile_changelist") + + f"?is_approved__exact=0&date_joined__date={timezone.now().date()}", + "number_of_users": models.Profile.users_awaiting_approval_count(), + "to_name": admin.first_name, } email = EmailMultiAlternatives( @@ -122,10 +122,13 @@ def send_admin_awaiting_approval_email(user, request, **kwargs): to=[admin.email], reply_to=[user.email], ) - css = finders.find('css/email.css') - html = Premailer(get_template("email/admin_awaiting_approval.html").render(context), - external_styles=css).transform() - email.attach_alternative(html, 'text/html') + css = finders.find("css/email.css") + html = Premailer( + get_template("email/admin_awaiting_approval.html").render(context), + external_styles=css, + allow_loading_external_files=True, + ).transform() + email.attach_alternative(html, "text/html") email.send() # Update last sent diff --git a/RIGS/static/js/marked.min.js b/RIGS/static/js/marked.min.js index 555c1dc1..4d2eb081 100644 --- a/RIGS/static/js/marked.min.js +++ b/RIGS/static/js/marked.min.js @@ -3,4 +3,4 @@ * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) * https://github.com/chjj/marked */ -(function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,block._tag)();block.paragraph=replace(block.paragraph)("hr",block.hr)("heading",block.heading)("lheading",block.lheading)("blockquote",block.blockquote)("tag","<"+block._tag)("def",block.def)();block.normal=merge({},block);block.gfm=merge({},block.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/});block.gfm.paragraph=replace(block.paragraph)("(?!","(?!"+block.gfm.fences.source.replace("\\1","\\2")+"|"+block.list.source.replace("\\1","\\3")+"|")();block.tables=merge({},block.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/});function Lexer(options){this.tokens=[];this.tokens.links={};this.options=options||marked.defaults;this.rules=block.normal;if(this.options.gfm){if(this.options.tables){this.rules=block.tables}else{this.rules=block.gfm}}}Lexer.rules=block;Lexer.lex=function(src,options){var lexer=new Lexer(options);return lexer.lex(src)};Lexer.prototype.lex=function(src){src=src.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n");return this.token(src,true)};Lexer.prototype.token=function(src,top,bq){var src=src.replace(/^ +$/gm,""),next,loose,cap,bull,b,item,space,i,l;while(src){if(cap=this.rules.newline.exec(src)){src=src.substring(cap[0].length);if(cap[0].length>1){this.tokens.push({type:"space"})}}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);cap=cap[0].replace(/^ {4}/gm,"");this.tokens.push({type:"code",text:!this.options.pedantic?cap.replace(/\n+$/,""):cap});continue}if(cap=this.rules.fences.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"code",lang:cap[2],text:cap[3]||""});continue}if(cap=this.rules.heading.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"heading",depth:cap[1].length,text:cap[2]});continue}if(top&&(cap=this.rules.nptable.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/\n$/,"").split("\n")};for(i=0;i ?/gm,"");this.token(cap,top,true);this.tokens.push({type:"blockquote_end"});continue}if(cap=this.rules.list.exec(src)){src=src.substring(cap[0].length);bull=cap[2];this.tokens.push({type:"list_start",ordered:bull.length>1});cap=cap[0].match(this.rules.item);next=false;l=cap.length;i=0;for(;i1&&b.length>1)){src=cap.slice(i+1).join("\n")+src;i=l-1}}loose=next||/\n\n(?!\s*$)/.test(item);if(i!==l-1){next=item.charAt(item.length-1)==="\n";if(!loose)loose=next}this.tokens.push({type:loose?"loose_item_start":"list_item_start"});this.token(item,false,bq);this.tokens.push({type:"list_item_end"})}this.tokens.push({type:"list_end"});continue}if(cap=this.rules.html.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&(cap[1]==="pre"||cap[1]==="script"||cap[1]==="style"),text:cap[0]});continue}if(!bq&&top&&(cap=this.rules.def.exec(src))){src=src.substring(cap[0].length);this.tokens.links[cap[1].toLowerCase()]={href:cap[2],title:cap[3]};continue}if(top&&(cap=this.rules.table.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/(?: *\| *)?\n$/,"").split("\n")};for(i=0;i])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:noop,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:noop,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/;inline.link=replace(inline.link)("inside",inline._inside)("href",inline._href)();inline.reflink=replace(inline.reflink)("inside",inline._inside)();inline.normal=merge({},inline);inline.pedantic=merge({},inline.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/});inline.gfm=merge({},inline.normal,{escape:replace(inline.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:replace(inline.text)("]|","~]|")("|","|https?://|")()});inline.breaks=merge({},inline.gfm,{br:replace(inline.br)("{2,}","*")(),text:replace(inline.gfm.text)("{2,}","*")()});function InlineLexer(links,options){this.options=options||marked.defaults;this.links=links;this.rules=inline.normal;this.renderer=this.options.renderer||new Renderer;this.renderer.options=this.options;if(!this.links){throw new Error("Tokens array requires a `links` property.")}if(this.options.gfm){if(this.options.breaks){this.rules=inline.breaks}else{this.rules=inline.gfm}}else if(this.options.pedantic){this.rules=inline.pedantic}}InlineLexer.rules=inline;InlineLexer.output=function(src,links,options){var inline=new InlineLexer(links,options);return inline.output(src)};InlineLexer.prototype.output=function(src){var out="",link,text,href,cap;while(src){if(cap=this.rules.escape.exec(src)){src=src.substring(cap[0].length);out+=cap[1];continue}if(cap=this.rules.autolink.exec(src)){src=src.substring(cap[0].length);if(cap[2]==="@"){text=cap[1].charAt(6)===":"?this.mangle(cap[1].substring(7)):this.mangle(cap[1]);href=this.mangle("mailto:")+text}else{text=escape(cap[1]);href=text}out+=this.renderer.link(href,null,text);continue}if(!this.inLink&&(cap=this.rules.url.exec(src))){src=src.substring(cap[0].length);text=escape(cap[1]);href=text;out+=this.renderer.link(href,null,text);continue}if(cap=this.rules.tag.exec(src)){if(!this.inLink&&/^/i.test(cap[0])){this.inLink=false}src=src.substring(cap[0].length);out+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(cap[0]):escape(cap[0]):cap[0];continue}if(cap=this.rules.link.exec(src)){src=src.substring(cap[0].length);this.inLink=true;out+=this.outputLink(cap,{href:cap[2],title:cap[3]});this.inLink=false;continue}if((cap=this.rules.reflink.exec(src))||(cap=this.rules.nolink.exec(src))){src=src.substring(cap[0].length);link=(cap[2]||cap[1]).replace(/\s+/g," ");link=this.links[link.toLowerCase()];if(!link||!link.href){out+=cap[0].charAt(0);src=cap[0].substring(1)+src;continue}this.inLink=true;out+=this.outputLink(cap,link);this.inLink=false;continue}if(cap=this.rules.strong.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.strong(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.em.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.em(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.codespan(escape(cap[2],true));continue}if(cap=this.rules.br.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.br();continue}if(cap=this.rules.del.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.del(this.output(cap[1]));continue}if(cap=this.rules.text.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.text(escape(this.smartypants(cap[0])));continue}if(src){throw new Error("Infinite loop on byte: "+src.charCodeAt(0))}}return out};InlineLexer.prototype.outputLink=function(cap,link){var href=escape(link.href),title=link.title?escape(link.title):null;return cap[0].charAt(0)!=="!"?this.renderer.link(href,title,this.output(cap[1])):this.renderer.image(href,title,escape(cap[1]))};InlineLexer.prototype.smartypants=function(text){if(!this.options.smartypants)return text;return text.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")};InlineLexer.prototype.mangle=function(text){if(!this.options.mangle)return text;var out="",l=text.length,i=0,ch;for(;i.5){ch="x"+ch.toString(16)}out+="&#"+ch+";"}return out};function Renderer(options){this.options=options||{}}Renderer.prototype.code=function(code,lang,escaped){if(this.options.highlight){var out=this.options.highlight(code,lang);if(out!=null&&out!==code){escaped=true;code=out}}if(!lang){return"
    "+(escaped?code:escape(code,true))+"\n
    "}return'
    '+(escaped?code:escape(code,true))+"\n
    \n"};Renderer.prototype.blockquote=function(quote){return"
    \n"+quote+"
    \n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"'+text+"\n"};Renderer.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"};Renderer.prototype.list=function(body,ordered){var type=ordered?"ol":"ul";return"<"+type+">\n"+body+"\n"};Renderer.prototype.listitem=function(text){return"
  • "+text+"
  • \n"};Renderer.prototype.paragraph=function(text){return"

    "+text+"

    \n"};Renderer.prototype.table=function(header,body){return"\n"+"\n"+header+"\n"+"\n"+body+"\n"+"
    \n"};Renderer.prototype.tablerow=function(content){return"\n"+content+"\n"};Renderer.prototype.tablecell=function(content,flags){var type=flags.header?"th":"td";var tag=flags.align?"<"+type+' style="text-align:'+flags.align+'">':"<"+type+">";return tag+content+"\n"};Renderer.prototype.strong=function(text){return""+text+""};Renderer.prototype.em=function(text){return""+text+""};Renderer.prototype.codespan=function(text){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?"
    ":"
    "};Renderer.prototype.del=function(text){return""+text+""};Renderer.prototype.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(prot.indexOf("javascript:")===0||prot.indexOf("vbscript:")===0){return""}}var out='
    ";return out};Renderer.prototype.image=function(href,title,text){var out=''+text+'":">";return out};Renderer.prototype.text=function(text){return text};function Parser(options){this.tokens=[];this.token=null;this.options=options||marked.defaults;this.options.renderer=this.options.renderer||new Renderer;this.renderer=this.options.renderer;this.renderer.options=this.options}Parser.parse=function(src,options,renderer){var parser=new Parser(options,renderer);return parser.parse(src)};Parser.prototype.parse=function(src){this.inline=new InlineLexer(src.links,this.options,this.renderer);this.tokens=src.reverse();var out="";while(this.next()){out+=this.tok()}return out};Parser.prototype.next=function(){return this.token=this.tokens.pop()};Parser.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};Parser.prototype.parseText=function(){var body=this.token.text;while(this.peek().type==="text"){body+="\n"+this.next().text}return this.inline.output(body)};Parser.prototype.tok=function(){switch(this.token.type){case"space":{return""}case"hr":{return this.renderer.hr()}case"heading":{return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text)}case"code":{return this.renderer.code(this.token.text,this.token.lang,this.token.escaped)}case"table":{var header="",body="",i,row,cell,flags,j;cell="";for(i=0;i/g,">").replace(/"/g,""").replace(/'/g,"'")}function unescape(html){return html.replace(/&([#\w]+);/g,function(_,n){n=n.toLowerCase();if(n==="colon")return":";if(n.charAt(0)==="#"){return n.charAt(1)==="x"?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1))}return""})}function replace(regex,opt){regex=regex.source;opt=opt||"";return function self(name,val){if(!name)return new RegExp(regex,opt);val=val.source||val;val=val.replace(/(^|[^\[])\^/g,"$1");regex=regex.replace(name,val);return self}}function noop(){}noop.exec=noop;function merge(obj){var i=1,target,key;for(;iAn error occured:

    "+escape(e.message+"",true)+"
    "}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,sanitizer:null,mangle:true,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); \ No newline at end of file +(function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,block._tag)();block.paragraph=replace(block.paragraph)("hr",block.hr)("heading",block.heading)("lheading",block.lheading)("blockquote",block.blockquote)("tag","<"+block._tag)("def",block.def)();block.normal=merge({},block);block.gfm=merge({},block.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/});block.gfm.paragraph=replace(block.paragraph)("(?!","(?!"+block.gfm.fences.source.replace("\\1","\\2")+"|"+block.list.source.replace("\\1","\\3")+"|")();block.tables=merge({},block.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/});function Lexer(options){this.tokens=[];this.tokens.links={};this.options=options||marked.defaults;this.rules=block.normal;if(this.options.gfm){if(this.options.tables){this.rules=block.tables}else{this.rules=block.gfm}}}Lexer.rules=block;Lexer.lex=function(src,options){var lexer=new Lexer(options);return lexer.lex(src)};Lexer.prototype.lex=function(src){src=src.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n");return this.token(src,true)};Lexer.prototype.token=function(src,top,bq){var src=src.replace(/^ +$/gm,""),next,loose,cap,bull,b,item,space,i,l;while(src){if(cap=this.rules.newline.exec(src)){src=src.substring(cap[0].length);if(cap[0].length>1){this.tokens.push({type:"space"})}}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);cap=cap[0].replace(/^ {4}/gm,"");this.tokens.push({type:"code",text:!this.options.pedantic?cap.replace(/\n+$/,""):cap});continue}if(cap=this.rules.fences.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"code",lang:cap[2],text:cap[3]||""});continue}if(cap=this.rules.heading.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"heading",depth:cap[1].length,text:cap[2]});continue}if(top&&(cap=this.rules.nptable.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/\n$/,"").split("\n")};for(i=0;i ?/gm,"");this.token(cap,top,true);this.tokens.push({type:"blockquote_end"});continue}if(cap=this.rules.list.exec(src)){src=src.substring(cap[0].length);bull=cap[2];this.tokens.push({type:"list_start",ordered:bull.length>1});cap=cap[0].match(this.rules.item);next=false;l=cap.length;i=0;for(;i1&&b.length>1)){src=cap.slice(i+1).join("\n")+src;i=l-1}}loose=next||/\n\n(?!\s*$)/.test(item);if(i!==l-1){next=item.charAt(item.length-1)==="\n";if(!loose)loose=next}this.tokens.push({type:loose?"loose_item_start":"list_item_start"});this.token(item,false,bq);this.tokens.push({type:"list_item_end"})}this.tokens.push({type:"list_end"});continue}if(cap=this.rules.html.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&(cap[1]==="pre"||cap[1]==="script"||cap[1]==="style"),text:cap[0]});continue}if(!bq&&top&&(cap=this.rules.def.exec(src))){src=src.substring(cap[0].length);this.tokens.links[cap[1].toLowerCase()]={href:cap[2],title:cap[3]};continue}if(top&&(cap=this.rules.table.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/(?: *\| *)?\n$/,"").split("\n")};for(i=0;i])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:noop,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:noop,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/;inline.link=replace(inline.link)("inside",inline._inside)("href",inline._href)();inline.reflink=replace(inline.reflink)("inside",inline._inside)();inline.normal=merge({},inline);inline.pedantic=merge({},inline.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/});inline.gfm=merge({},inline.normal,{escape:replace(inline.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:replace(inline.text)("]|","~]|")("|","|https?://|")()});inline.breaks=merge({},inline.gfm,{br:replace(inline.br)("{2,}","*")(),text:replace(inline.gfm.text)("{2,}","*")()});function InlineLexer(links,options){this.options=options||marked.defaults;this.links=links;this.rules=inline.normal;this.renderer=this.options.renderer||new Renderer;this.renderer.options=this.options;if(!this.links){throw new Error("Tokens array requires a `links` property.")}if(this.options.gfm){if(this.options.breaks){this.rules=inline.breaks}else{this.rules=inline.gfm}}else if(this.options.pedantic){this.rules=inline.pedantic}}InlineLexer.rules=inline;InlineLexer.output=function(src,links,options){var inline=new InlineLexer(links,options);return inline.output(src)};InlineLexer.prototype.output=function(src){var out="",link,text,href,cap;while(src){if(cap=this.rules.escape.exec(src)){src=src.substring(cap[0].length);out+=cap[1];continue}if(cap=this.rules.autolink.exec(src)){src=src.substring(cap[0].length);if(cap[2]==="@"){text=cap[1].charAt(6)===":"?this.mangle(cap[1].substring(7)):this.mangle(cap[1]);href=this.mangle("mailto:")+text}else{text=escape(cap[1]);href=text}out+=this.renderer.link(href,null,text);continue}if(!this.inLink&&(cap=this.rules.url.exec(src))){src=src.substring(cap[0].length);text=escape(cap[1]);href=text;out+=this.renderer.link(href,null,text);continue}if(cap=this.rules.tag.exec(src)){if(!this.inLink&&/^
    /i.test(cap[0])){this.inLink=false}src=src.substring(cap[0].length);out+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(cap[0]):escape(cap[0]):cap[0];continue}if(cap=this.rules.link.exec(src)){src=src.substring(cap[0].length);this.inLink=true;out+=this.outputLink(cap,{href:cap[2],title:cap[3]});this.inLink=false;continue}if((cap=this.rules.reflink.exec(src))||(cap=this.rules.nolink.exec(src))){src=src.substring(cap[0].length);link=(cap[2]||cap[1]).replace(/\s+/g," ");link=this.links[link.toLowerCase()];if(!link||!link.href){out+=cap[0].charAt(0);src=cap[0].substring(1)+src;continue}this.inLink=true;out+=this.outputLink(cap,link);this.inLink=false;continue}if(cap=this.rules.strong.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.strong(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.em.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.em(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.codespan(escape(cap[2],true));continue}if(cap=this.rules.br.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.br();continue}if(cap=this.rules.del.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.del(this.output(cap[1]));continue}if(cap=this.rules.text.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.text(escape(this.smartypants(cap[0])));continue}if(src){throw new Error("Infinite loop on byte: "+src.charCodeAt(0))}}return out};InlineLexer.prototype.outputLink=function(cap,link){var href=escape(link.href),title=link.title?escape(link.title):null;return cap[0].charAt(0)!=="!"?this.renderer.link(href,title,this.output(cap[1])):this.renderer.image(href,title,escape(cap[1]))};InlineLexer.prototype.smartypants=function(text){if(!this.options.smartypants)return text;return text.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")};InlineLexer.prototype.mangle=function(text){if(!this.options.mangle)return text;var out="",l=text.length,i=0,ch;for(;i.5){ch="x"+ch.toString(16)}out+="&#"+ch+";"}return out};function Renderer(options){this.options=options||{}}Renderer.prototype.code=function(code,lang,escaped){if(this.options.highlight){var out=this.options.highlight(code,lang);if(out!=null&&out!==code){escaped=true;code=out}}if(!lang){return"
    "+(escaped?code:escape(code,true))+"\n
    "}return'
    '+(escaped?code:escape(code,true))+"\n
    \n"};Renderer.prototype.blockquote=function(quote){return"
    \n"+quote+"
    \n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"'+text+"\n"};Renderer.prototype.hr=function(){return this.options.xhtml?"
    \n":"
    \n"};Renderer.prototype.list=function(body,ordered){var type=ordered?"ol":"ul";return"<"+type+">\n"+body+"\n"};Renderer.prototype.listitem=function(text){return"
  • "+text+"
  • \n"};Renderer.prototype.paragraph=function(text){return"

    "+text+"

    \n"};Renderer.prototype.table=function(header,body){return"\n"+"\n"+header+"\n"+"\n"+body+"\n"+"
    \n"};Renderer.prototype.tablerow=function(content){return"\n"+content+"\n"};Renderer.prototype.tablecell=function(content,flags){var type=flags.header?"th":"td";var tag=flags.align?"<"+type+' style="text-align:'+flags.align+'">':"<"+type+">";return tag+content+"\n"};Renderer.prototype.strong=function(text){return""+text+""};Renderer.prototype.em=function(text){return""+text+""};Renderer.prototype.codespan=function(text){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?"
    ":"
    "};Renderer.prototype.del=function(text){return""+text+""};Renderer.prototype.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(prot.indexOf("javascript:")===0||prot.indexOf("vbscript:")===0){return""}}var out='
    ";return out};Renderer.prototype.image=function(href,title,text){var out=''+text+'":">";return out};Renderer.prototype.text=function(text){return text};function Parser(options){this.tokens=[];this.token=null;this.options=options||marked.defaults;this.options.renderer=this.options.renderer||new Renderer;this.renderer=this.options.renderer;this.renderer.options=this.options}Parser.parse=function(src,options,renderer){var parser=new Parser(options,renderer);return parser.parse(src)};Parser.prototype.parse=function(src){this.inline=new InlineLexer(src.links,this.options,this.renderer);this.tokens=src.reverse();var out="";while(this.next()){out+=this.tok()}return out};Parser.prototype.next=function(){return this.token=this.tokens.pop()};Parser.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};Parser.prototype.parseText=function(){var body=this.token.text;while(this.peek().type==="text"){body+="\n"+this.next().text}return this.inline.output(body)};Parser.prototype.tok=function(){switch(this.token.type){case"space":{return""}case"hr":{return this.renderer.hr()}case"heading":{return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text)}case"code":{return this.renderer.code(this.token.text,this.token.lang,this.token.escaped)}case"table":{var header="",body="",i,row,cell,flags,j;cell="";for(i=0;i/g,">").replace(/"/g,""").replace(/'/g,"'")}function unescape(html){return html.replace(/&([#\w]+);/g,function(_,n){n=n.toLowerCase();if(n==="colon")return":";if(n.charAt(0)==="#"){return n.charAt(1)==="x"?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1))}return""})}function replace(regex,opt){regex=regex.source;opt=opt||"";return function self(name,val){if(!name)return new RegExp(regex,opt);val=val.source||val;val=val.replace(/(^|[^\[])\^/g,"$1");regex=regex.replace(name,val);return self}}function noop(){}noop.exec=noop;function merge(obj){var i=1,target,key;for(;iAn error occured:

    "+escape(e.message+"",true)+"
    "}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,sanitizer:null,mangle:true,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); diff --git a/RIGS/templates/base_rigs.html b/RIGS/templates/base_rigs.html index f9a78c9d..d7556a4e 100644 --- a/RIGS/templates/base_rigs.html +++ b/RIGS/templates/base_rigs.html @@ -6,36 +6,8 @@ {% load total_invoices_todo from filters %} {% block titleheader %} - - -
    FrankenRIGS +RIGS {% endblock %} {% block titleelements %} diff --git a/RIGS/templates/email/eventauthorisation_mic_success.txt b/RIGS/templates/email/eventauthorisation_mic_success.txt index b4309dd4..43ddd5e1 100644 --- a/RIGS/templates/email/eventauthorisation_mic_success.txt +++ b/RIGS/templates/email/eventauthorisation_mic_success.txt @@ -1,5 +1,5 @@ Hi {{object.event.mic.get_full_name|default_if_none:"somebody"}}, -Just to let you know your event N{{object.eventdisplay_id}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}. +Just to let you know your event N{{object.event.pk|stringformat:"05d"}} has been successfully authorised for £{{object.amount}} by {{object.name}} as of {{object.event.last_edited_at}}. The TEC Rig Information Gathering System diff --git a/RIGS/templates/event_detail.html b/RIGS/templates/event_detail.html index 816dee1e..4013df65 100644 --- a/RIGS/templates/event_detail.html +++ b/RIGS/templates/event_detail.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} +{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% load markdown_tags %} {% load static %} @@ -18,7 +18,7 @@ {% block content %}
    - {% if not request.is_ajax %} + {% if not is_ajax %} {% if perms.RIGS.view_event %}
    {% include 'partials/event_detail_buttons.html' %} @@ -49,7 +49,7 @@
    {% endif %} {% endif %} - {% if not request.is_ajax and perms.RIGS.view_event %} + {% if not is_ajax and perms.RIGS.view_event %}
    {% include 'partials/event_detail_buttons.html' %}
    @@ -69,16 +69,16 @@
    - + {% include 'partials/crew_list.html' %} - {% if not request.is_ajax and perms.RIGS.view_event %} + {% if not is_ajax and perms.RIGS.view_event %}
    {% include 'partials/event_detail_buttons.html' %}
    {% endif %} {% endif %} - {% if not request.is_ajax and perms.RIGS.view_event %} + {% if not is_ajax and perms.RIGS.view_event %}
    {% include 'partials/last_edited.html' with target="event_history" %}
    @@ -86,7 +86,7 @@ {% endblock %} -{% if request.is_ajax %} +{% if is_ajax %} {% block footer %} {% if perms.RIGS.view_event %} {% include 'partials/last_edited.html' with target="event_history" %} diff --git a/RIGS/templates/eventauthorisation_request.html b/RIGS/templates/eventauthorisation_request.html index ae2e837c..a8adc86f 100644 --- a/RIGS/templates/eventauthorisation_request.html +++ b/RIGS/templates/eventauthorisation_request.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %} {% load widget_tweaks %} {% load static %} {% load button from filters %} diff --git a/RIGS/templates/eventauthorisation_request_error.html b/RIGS/templates/eventauthorisation_request_error.html index 863271eb..3ac9dda3 100644 --- a/RIGS/templates/eventauthorisation_request_error.html +++ b/RIGS/templates/eventauthorisation_request_error.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %} {% load widget_tweaks %} {% block title %}TEC Email Address Required{% endblock %} diff --git a/RIGS/templates/hs/event_checklist_detail.html b/RIGS/templates/hs/event_checklist_detail.html index 21a37c03..e3c69ceb 100644 --- a/RIGS/templates/hs/event_checklist_detail.html +++ b/RIGS/templates/hs/event_checklist_detail.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} +{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% load help_text from filters %} {% load profile_by_index from filters %} {% load yesnoi from filters %} diff --git a/RIGS/templates/hs/event_checklist_form.html b/RIGS/templates/hs/event_checklist_form.html index 373ad306..5e138dce 100644 --- a/RIGS/templates/hs/event_checklist_form.html +++ b/RIGS/templates/hs/event_checklist_form.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %} {% load widget_tweaks %} {% load static %} {% load help_text from filters %} diff --git a/RIGS/templates/hs/eventcheckin_form.html b/RIGS/templates/hs/eventcheckin_form.html index f6c80995..cb2be156 100644 --- a/RIGS/templates/hs/eventcheckin_form.html +++ b/RIGS/templates/hs/eventcheckin_form.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %} {% load widget_tweaks %} {% load static %} {% load button from filters %} @@ -28,14 +28,16 @@
    - {% if not request.is_ajax and self.request.user.pk is form.event.mic.pk %} + {% if not is_ajax and request.user.pk == event.mic.pk %}
    @@ -86,7 +88,7 @@
    {% endif %} - {% if not request.is_ajax %} + {% if not is_ajax %}
    {% button 'submit' %} diff --git a/RIGS/templates/hs/hs_object_list.html b/RIGS/templates/hs/hs_object_list.html new file mode 100644 index 00000000..c3ebaf4d --- /dev/null +++ b/RIGS/templates/hs/hs_object_list.html @@ -0,0 +1,59 @@ +{% extends 'base_rigs.html' %} +{% load paginator from filters %} +{% load help_text from filters %} +{% load verbose_name from filters %} +{% load get_field from filters %} + +{% block title %}{{ title }} List{% endblock %} + +{% block content %} +
    +
    +

    {{title}} List

    +
    +
    +
    +
    +
    + + + + + {# mmm hax #} + {% if object_list.0 != None %} + {% for field in object_list.0.fieldz %} + + {% endfor %} + {% endif %} + + + + + {% for object in object_list %} + + {# General #} + + {% for field in object_list.0.fieldz %} + + {% endfor %} + {# Buttons #} + + + {% empty %} + + + + {% endfor %} + +
    Event{{ object_list.0|verbose_name:field|title }}
    {{ object.event }}
    {{ object.event.get_status_display }}
    {{ object|get_field:field }} + {% include 'partials/hs_status.html' %} +
    Nothing found
    +
    +
    +
    + {% if is_paginated %} +
    + {% paginator %} +
    + {% endif %} +{% endblock %} diff --git a/RIGS/templates/hs/power_detail.html b/RIGS/templates/hs/power_detail.html index f130ddad..2fd123f6 100644 --- a/RIGS/templates/hs/power_detail.html +++ b/RIGS/templates/hs/power_detail.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} +{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% load help_text from filters %} {% load profile_by_index from filters %} {% load yesnoi from filters %} diff --git a/RIGS/templates/hs/power_form.html b/RIGS/templates/hs/power_form.html index d77ead1b..e07877e0 100644 --- a/RIGS/templates/hs/power_form.html +++ b/RIGS/templates/hs/power_form.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %} {% load widget_tweaks %} {% load static %} {% load help_text from filters %} diff --git a/RIGS/templates/hs/power_print.xml b/RIGS/templates/hs/power_print.xml index d5b2fb6f..f8126197 100644 --- a/RIGS/templates/hs/power_print.xml +++ b/RIGS/templates/hs/power_print.xml @@ -50,7 +50,7 @@ {% elif object.event.riskassessment.event_size == 2 %} Large Event {% endif %} - + diff --git a/RIGS/templates/hs/risk_assessment_detail.html b/RIGS/templates/hs/risk_assessment_detail.html index a933e152..146d2ddc 100644 --- a/RIGS/templates/hs/risk_assessment_detail.html +++ b/RIGS/templates/hs/risk_assessment_detail.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} +{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% load filters %} {% block content %} diff --git a/RIGS/templates/hs/risk_assessment_form.html b/RIGS/templates/hs/risk_assessment_form.html index 572d8b31..cccef9bd 100644 --- a/RIGS/templates/hs/risk_assessment_form.html +++ b/RIGS/templates/hs/risk_assessment_form.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_rigs.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_rigs.html' %} {% load widget_tweaks %} {% load static %} {% load help_text from filters %} diff --git a/RIGS/templates/partials/contact_details.html b/RIGS/templates/partials/contact_details.html index 202770e8..3738ce87 100644 --- a/RIGS/templates/partials/contact_details.html +++ b/RIGS/templates/partials/contact_details.html @@ -44,4 +44,4 @@
    -{% endif %} \ No newline at end of file +{% endif %} diff --git a/RIGS/templates/partials/yes_no_radio.html b/RIGS/templates/partials/yes_no_radio.html index 05ef6458..d8759e6b 100644 --- a/RIGS/templates/partials/yes_no_radio.html +++ b/RIGS/templates/partials/yes_no_radio.html @@ -1,7 +1,7 @@
    - -
    +
    {% for radio in formitem %}
    {{ radio.tag }} diff --git a/RIGS/templatetags/filters.py b/RIGS/templatetags/filters.py index 73a8c148..e99355f2 100644 --- a/RIGS/templatetags/filters.py +++ b/RIGS/templatetags/filters.py @@ -21,7 +21,7 @@ def linebreaksxml(value, autoescape=True): value = normalize_newlines(value) if autoescape: value = escape(value) - return mark_safe(value.replace('\n', '
    ')) + return mark_safe(value.replace("\n", "
    ")) @register.filter @@ -35,7 +35,7 @@ def to_class_name(value): @register.filter(needs_autoescape=True) -def nice_errors(form, non_field_msg='General form errors', autoescape=True): +def nice_errors(form, non_field_msg="General form errors", autoescape=True): nice_errors = ErrorDict() if isinstance(form, forms.BaseForm): for field, errors in list(form.errors.items()): @@ -47,7 +47,7 @@ def nice_errors(form, non_field_msg='General form errors', autoescape=True): return nice_errors -@register.inclusion_tag('pagination.html', takes_context=True) +@register.inclusion_tag("pagination.html", takes_context=True) def paginator(context, adjacent_pages=3): """ To be used in conjunction with the object_list generic view. @@ -57,36 +57,35 @@ def paginator(context, adjacent_pages=3): view. """ - page = context['page_obj'] - paginator = context['paginator'] + page = context["page_obj"] + paginator = context["paginator"] startPage = max(page.number - adjacent_pages, 1) if startPage <= 3: startPage = 1 endPage = page.number + adjacent_pages + 1 if endPage >= paginator.num_pages - 1: endPage = paginator.num_pages + 1 - page_numbers = [n for n in range(startPage, endPage) - if n > 0 and n <= paginator.num_pages] + page_numbers = [n for n in range(startPage, endPage) if n > 0 and n <= paginator.num_pages] dict = { - 'request': context['request'], - 'is_paginated': paginator.num_pages > 0, - 'page_obj': page, - 'paginator': paginator, - 'results': paginator.per_page, - 'page_numbers': page_numbers, - 'show_first': 1 not in page_numbers, - 'show_last': paginator.num_pages not in page_numbers, - 'first': 1, - 'last': paginator.num_pages, - 'has_next': page.has_next(), - 'has_previous': page.has_previous(), + "request": context["request"], + "is_paginated": paginator.num_pages > 0, + "page_obj": page, + "paginator": paginator, + "results": paginator.per_page, + "page_numbers": page_numbers, + "show_first": 1 not in page_numbers, + "show_last": paginator.num_pages not in page_numbers, + "first": 1, + "last": paginator.num_pages, + "has_next": page.has_next(), + "has_previous": page.has_previous(), } if page.has_next(): - dict['next'] = page.next_page_number() + dict["next"] = page.next_page_number() if page.has_previous(): - dict['previous'] = page.previous_page_number() + dict["previous"] = page.previous_page_number() return dict @@ -115,25 +114,27 @@ def orderby(request, field, attr): return dict_.urlencode() -@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist +@register.filter( + needs_autoescape=True +) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist def get_field(obj, field, autoescape=True): value = getattr(obj, field) - if (isinstance(value, bool)): + if isinstance(value, bool): value = yesnoi(value, field in obj.inverted_fields) - elif (isinstance(value, str)): + elif isinstance(value, str): value = truncatewords(value, 20) return mark_safe(value) @register.filter def help_text(obj, field): - if hasattr(obj, '_meta'): + if hasattr(obj, "_meta"): return obj._meta.get_field(field).help_text @register.filter def verbose_name(obj, field): - if hasattr(obj._meta.get_field(field), 'verbose_name'): + if hasattr(obj._meta.get_field(field), "verbose_name"): return obj._meta.get_field(field).verbose_name @@ -144,7 +145,7 @@ def get_list(dictionary, key): @register.filter def profile_by_index(value): - if (value): + if value: return models.Profile.objects.get(pk=int(value)) else: return "" @@ -165,13 +166,15 @@ def yesnoi(boolean, invert=False, autoescape=True): @register.filter @stringfilter def title_spaced(string): - return title(string).replace('_', ' ') + return title(string).replace("_", " ") @register.filter(needs_autoescape=True) def namewithnotes(obj, url, autoescape=True): - if hasattr(obj, 'notes') and obj.notes is not None and len(obj.notes) > 0: - return mark_safe(obj.name + f" ") + if hasattr(obj, "notes") and obj.notes is not None and len(obj.notes) > 0: + return mark_safe( + obj.name + f" " + ) else: return obj.name @@ -188,37 +191,37 @@ def linkornone(target, namespace=None, autoescape=True): return "None" -@register.inclusion_tag('partials/button.html') +@register.inclusion_tag("partials/button.html") def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style=None): - if type == 'edit': + if type == "edit": clazz += " btn-warning " icon = "fa-edit" text = "Edit" - elif type == 'print': + elif type == "print": clazz += " btn-primary " icon = "fa-download" text = "Export" - elif type == 'duplicate': + elif type == "duplicate": clazz += " btn-info " icon = "fa-copy" text = "Duplicate" - elif type == 'view': + elif type == "view": clazz += " btn-primary " icon = "fa-eye" text = "View " + text - elif type == 'new': + elif type == "new": clazz += " btn-primary " icon = "fa-plus" text = "New" - elif type == 'copy': - return {'copy': True, 'id': id, 'style': style} - elif type == 'search': - return {'submit': True, 'class': 'btn-info', 'icon': 'fa-search', 'text': 'Search', 'id': id, 'style': style} - elif type == 'submit': - return {'submit': True, 'class': 'btn-primary', 'icon': 'fa-save', 'text': 'Save', 'id': id, 'style': style} - elif type == 'today': - return {'today': True, 'id': id} - return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style} + elif type == "copy": + return {"copy": True, "id": id, "style": style} + elif type == "search": + return {"submit": True, "class": "btn-info", "icon": "fa-search", "text": "Search", "id": id, "style": style} + elif type == "submit": + return {"submit": True, "class": "btn-primary", "icon": "fa-save", "text": "Save", "id": id, "style": style} + elif type == "today": + return {"today": True, "id": id} + return {"target": url, "pk": pk, "class": clazz, "icon": icon, "text": text, "id": id, "style": style} @register.simple_tag # TODO Can these be done with annotation/aggregation? diff --git a/RIGS/templatetags/markdown_tags.py b/RIGS/templatetags/markdown_tags.py index 103c08e2..0f8bd058 100644 --- a/RIGS/templatetags/markdown_tags.py +++ b/RIGS/templatetags/markdown_tags.py @@ -3,54 +3,53 @@ from django import template from django.utils.safestring import mark_safe import markdown -__author__ = 'ghost' +__author__ = "ghost" register = template.Library() @register.filter(name="markdown") -def markdown_filter(text, input_format='html', add_style=""): +def markdown_filter(text, input_format="html", add_style=""): # markdown library can't handle text=None if text is None: return text - html = markdown.markdown(text, extensions=['markdown.extensions.nl2br']) + html = markdown.markdown(text, extensions=["markdown.extensions.nl2br"]) # Convert format to RML soup = BeautifulSoup(html, "html.parser") # Prevent code injection - for script in soup('script'): + for script in soup("script"): script.string = "Your script shall not pass!" - if input_format == 'html': - return mark_safe('
    ' + str(soup) + '
    ') - elif input_format == 'rml': - + if input_format == "html": + return mark_safe('
    ' + str(soup) + "
    ") + elif input_format == "rml": # Image aren't supported so remove them - for img in soup('img'): + for img in soup("img"): img.parent.extract() # should become - for c in soup('code'): - c.name = 'font' - c['face'] = "Courier" + for c in soup("code"): + c.name = "font" + c["face"] = "Courier" # blockquotes don't exist but we can still do something to show - for bq in soup('blockquote'): - bq.name = 'pre' + for bq in soup("blockquote"): + bq.name = "pre" bq.string = bq.text - for alist in soup.find_all(['ul', 'ol']): - alist['style'] = alist.name - for li in alist.find_all('li', recursive=False): - text = li.find(text=True) - text.wrap(soup.new_tag('p')) + for alist in soup.find_all(["ul", "ol"]): + alist["style"] = alist.name + for li in alist.find_all("li", recursive=False): + text = li.find(string=True) + text.wrap(soup.new_tag("p")) - if alist.parent.name != 'li': - indent = soup.new_tag('indent') - indent['left'] = '0.6cm' + if alist.parent.name != "li": + indent = soup.new_tag("indent") + indent["left"] = "0.6cm" alist.wrap(indent) # Paragraphs have a different tag - for p in soup('p'): - p.name = 'para' + for p in soup("p"): + p.name = "para" return mark_safe(str(soup)) diff --git a/RIGS/tests/conftest.py b/RIGS/tests/conftest.py index 3d7be854..9bbf2750 100644 --- a/RIGS/tests/conftest.py +++ b/RIGS/tests/conftest.py @@ -1,3 +1,5 @@ +from datetime import date, timedelta + from RIGS import models import pytest from django.utils import timezone @@ -12,15 +14,31 @@ def basic_event(db): @pytest.fixture def ra(basic_event, admin_user): - ra = models.RiskAssessment.objects.create(event=basic_event, nonstandard_equipment=False, nonstandard_use=False, - contractors=False, other_companies=False, crew_fatigue=False, - big_power=False, power_mic=admin_user, generators=False, - other_companies_power=False, nonstandard_equipment_power=False, - multiple_electrical_environments=False, noise_monitoring=False, - known_venue=True, safe_loading=True, safe_storage=True, - area_outside_of_control=True, barrier_required=True, - nonstandard_emergency_procedure=True, special_structures=False, - suspended_structures=False, outside=False, parking_and_access=False) + ra = models.RiskAssessment.objects.create( + event=basic_event, + nonstandard_equipment=False, + nonstandard_use=False, + contractors=False, + other_companies=False, + crew_fatigue=False, + big_power=False, + power_mic=admin_user, + generators=False, + other_companies_power=False, + nonstandard_equipment_power=False, + multiple_electrical_environments=False, + noise_monitoring=False, + known_venue=True, + safe_loading=True, + safe_storage=True, + area_outside_of_control=True, + barrier_required=True, + nonstandard_emergency_procedure=True, + special_structures=False, + suspended_structures=False, + outside=False, + parking_and_access=False, + ) yield ra ra.delete() @@ -43,11 +61,19 @@ def venue(db): @pytest.fixture # TODO parameterise with Event sizes def checklist(basic_event, venue, admin_user, ra): - checklist = models.EventChecklist.objects.create(event=basic_event, safe_parking=False, - safe_packing=False, exits=False, trip_hazard=False, warning_signs=False, - ear_plugs=False, hs_location="Locked away safely", - extinguishers_location="Somewhere, I forgot", - date=timezone.now(), venue=venue) + checklist = models.EventChecklist.objects.create( + event=basic_event, + safe_parking=False, + safe_packing=False, + exits=False, + trip_hazard=False, + warning_signs=False, + ear_plugs=False, + hs_location="Locked away safely", + extinguishers_location="Somewhere, I forgot", + date=timezone.now(), + venue=venue, + ) yield checklist checklist.delete() @@ -63,52 +89,93 @@ def power_test(basic_event, venue, admin_user, ra): def many_events(db, admin_user, scope="class"): many_events = { # produce 7 normal events - 5 current - 1: models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6), - description="start future no end"), + 1: models.Event.objects.create( + name="TE E1", start_date=date.today() + timedelta(days=6), description="start future no end" + ), 2: models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end"), - 3: models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(), - description="start today with end today"), - 4: models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end"), - 5: models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21', - description="start past with end past"), - 6: models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2), - end_date=date.today() + timedelta(days=2), - description="start past, end future"), - 7: models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2), - end_date=date.today() + timedelta(days=2), - description="start + end in future"), - + 3: models.Event.objects.create( + name="TE E3", start_date=date.today(), end_date=date.today(), description="start today with end today" + ), + 4: models.Event.objects.create(name="TE E4", start_date="2014-03-20", description="start past no end"), + 5: models.Event.objects.create( + name="TE E5", start_date="2014-03-20", end_date="2014-03-21", description="start past with end past" + ), + 6: models.Event.objects.create( + name="TE E6", + start_date=date.today() - timedelta(days=2), + end_date=date.today() + timedelta(days=2), + description="start past, end future", + ), + 7: models.Event.objects.create( + name="TE E7", + start_date=date.today() + timedelta(days=2), + end_date=date.today() + timedelta(days=2), + description="start + end in future", + ), # 2 cancelled - 1 current - 8: models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2), - end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED, - description="cancelled in future"), - 9: models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1), - end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED, - description="cancelled and started"), - + 8: models.Event.objects.create( + name="TE E8", + start_date=date.today() + timedelta(days=2), + end_date=date.today() + timedelta(days=2), + status=models.Event.CANCELLED, + description="cancelled in future", + ), + 9: models.Event.objects.create( + name="TE E9", + start_date=date.today() - timedelta(days=1), + end_date=date.today() + timedelta(days=2), + status=models.Event.CANCELLED, + description="cancelled and started", + ), # 5 dry hire - 3 current - 10: models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, - description="dryhire today"), - 11: models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, - checked_in_by=admin_user, - description="dryhire today, checked in"), - 12: models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True, - status=models.Event.BOOKED, description="dryhire past"), - 13: models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True, - checked_in_by=admin_user, description="dryhire past checked in"), - 14: models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True, - status=models.Event.CANCELLED, description="dryhire today cancelled"), - + 10: models.Event.objects.create( + name="TE E10", start_date=date.today(), dry_hire=True, description="dryhire today" + ), + 11: models.Event.objects.create( + name="TE E11", + start_date=date.today(), + dry_hire=True, + checked_in_by=admin_user, + description="dryhire today, checked in", + ), + 12: models.Event.objects.create( + name="TE E12", + start_date=date.today() - timedelta(days=1), + dry_hire=True, + status=models.Event.BOOKED, + description="dryhire past", + ), + 13: models.Event.objects.create( + name="TE E13", + start_date=date.today() - timedelta(days=2), + dry_hire=True, + checked_in_by=admin_user, + description="dryhire past checked in", + ), + 14: models.Event.objects.create( + name="TE E14", + start_date=date.today(), + dry_hire=True, + status=models.Event.CANCELLED, + description="dryhire today cancelled", + ), # 4 non rig - 3 current - 15: models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False, - description="non rig today"), - 16: models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False, - description="non rig tomorrow"), - 17: models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False, - description="non rig yesterday"), - 18: models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, - status=models.Event.CANCELLED, - description="non rig today cancelled"), + 15: models.Event.objects.create( + name="TE E15", start_date=date.today(), is_rig=False, description="non rig today" + ), + 16: models.Event.objects.create( + name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False, description="non rig tomorrow" + ), + 17: models.Event.objects.create( + name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False, description="non rig yesterday" + ), + 18: models.Event.objects.create( + name="TE E18", + start_date=date.today(), + is_rig=False, + status=models.Event.CANCELLED, + description="non rig today cancelled", + ), } yield many_events for event in many_events: diff --git a/RIGS/tests/pages.py b/RIGS/tests/pages.py deleted file mode 100644 index c6d9da24..00000000 --- a/RIGS/tests/pages.py +++ /dev/null @@ -1,358 +0,0 @@ -from django.urls import reverse -from pypom import Region -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as EC - -from PyRIGS.tests import regions -from PyRIGS.tests.pages import BasePage, FormPage -from RIGS.tests import regions as rigs_regions - - -class Index(BasePage): - URL_TEMPLATE = reverse('index') - - -class Rigboard(BasePage): - URL_TEMPLATE = reverse('rigboard') - - _add_item_selector = (By.XPATH, "//a[contains(@class,'btn-primary') and contains(., 'New')]") - _event_row_locator = (By.CLASS_NAME, "event-row") - - def add(self): - self.find_element(*self._add_item_selector).click() - - class EventListRow(Region): - _event_number_locator = (By.ID, "event_number") - _event_dates_locator = (By.CLASS_NAME, "event-dates") - _event_details_locator = (By.ID, "event_details") - _event_mic_locator = (By.ID, "event_mic") - - @property - def id(self): - return self.find_element(*self._event_number_locator).text - - @property - def dates(self): - return self.find_element(*self._event_dates_locator).text - - @property - def details(self): - return self.find_element(*self._event_details_locator).text - - @property - def mic(self): - return self.find_element(*self._event_mic_locator).text - - @property - def events(self): - return [self.EventListRow(self, i) for i in self.find_elements(*self._event_row_locator)] - - -class EventDetail(BasePage): - URL_TEMPLATE = 'event/{event_id}' - - # TODO Refactor into regions to match template fragmentation - _event_name_selector = (By.XPATH, '//h2') - _person_panel_selector = (By.XPATH, '//div[contains(text(), "Person Details")]/..') - _name_selector = (By.XPATH, '//dt[text()="Person"]/following-sibling::dd[1]') - _email_selector = (By.XPATH, '//dt[text()="Email"]/following-sibling::dd[1]') - _phone_selector = (By.XPATH, '//dt[text()="Phone Number"]/following-sibling::dd[1]') - _event_table_selector = (By.ID, 'item-table') - - @property - def event_name(self): - return self.find_element(*self._event_name_selector).text - - @property - def name(self): - return self.find_element(*self._person_panel_selector).find_element(*self._name_selector).text - - @property - def email(self): - return self.find_element(*self._person_panel_selector).find_element(*self._email_selector).text - - @property - def phone(self): - return self.find_element(*self._person_panel_selector).find_element(*self._phone_selector).text - - @property - def item_table(self): - return self.find_element(*self._event_table_selector) - - -class CreateEvent(FormPage): - URL_TEMPLATE = reverse('event_create') - - _is_rig_selector = (By.ID, 'is_rig-selector') - _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]") - _person_selector_selector = (By.XPATH, '//*[@id="id_person"]/..') - _venue_selector_selector = (By.XPATH, '//*[@id="id_venue"]/..') - _mic_selector_selector = (By.XPATH, '//*[@id="form-hws"]/div[7]/div[1]/div/div') - - _add_person_selector = (By.XPATH, '//a[@data-target="#id_person" and contains(@href, "add")]') - _add_item_selector = (By.XPATH, '//button[contains(@class, "item-add")]') - - _event_table_selector = (By.ID, 'item-table') - _warning_selector = (By.XPATH, '/html/body/div[1]/div[1]') - - form_items = { - 'description': (regions.SimpleMDETextArea, (By.ID, 'id_description')), - - 'name': (regions.TextBox, (By.ID, 'id_name')), - 'start_date': (regions.DatePicker, (By.ID, 'id_start_date')), - 'start_time': (regions.TimePicker, (By.ID, 'id_start_time')), - 'end_date': (regions.DatePicker, (By.ID, 'id_end_date')), - 'end_time': (regions.TimePicker, (By.ID, 'id_end_time')), - 'access_at': (regions.DateTimePicker, (By.ID, 'id_access_at')), - 'meet_at': (regions.DateTimePicker, (By.ID, 'id_meet_at')), - 'dry_hire': (regions.CheckBox, (By.ID, 'id_dry_hire')), - 'status': (regions.SingleSelectPicker, (By.ID, 'id_status')), - 'collected_by': (regions.TextBox, (By.ID, 'id_collector')), - 'po': (regions.TextBox, (By.ID, 'id_purchase_order')), - - 'notes': (regions.SimpleMDETextArea, (By.ID, 'id_notes')) - } - - def select_event_type(self, type_name): - self.find_element(By.XPATH, f'//button[.="{type_name}"]').click() - - def item_row(self, ID): - return rigs_regions.ItemRow(self, self.find_element(By.ID, "item-" + ID)) - - @property - def item_table(self): - return self.find_element(*self._event_table_selector) - - @property - def warning(self): - return self.find_element(*self._warning_selector).text - - @property - def is_expanded(self): - return self.find_element(*self._submit_locator).is_displayed() - - @property - def person_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._person_selector_selector)) - - @property - def venue_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._venue_selector_selector)) - - @property - def mic_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._mic_selector_selector)) - - def add_person(self): - self.find_element(*self._add_person_selector).click() - return regions.Modal(self, self.driver.find_element(By.ID, 'modal')) - - def add_event_item(self): - self.find_element(*self._add_item_selector).click() - element = self.driver.find_element(By.ID, 'itemModal') - self.wait.until(EC.visibility_of(element)) - return rigs_regions.ItemModal(self, element) - - @property - def success(self): - return '/create' not in self.driver.current_url - - -class DuplicateEvent(CreateEvent): - URL_TEMPLATE = 'event/{event_id}/duplicate' - - @property - def success(self): - return '/duplicate' not in self.driver.current_url - - -class EditEvent(CreateEvent): - URL_TEMPLATE = 'event/{event_id}/edit' - - @property - def success(self): - return '/edit' not in self.driver.current_url - - -class CreateRiskAssessment(FormPage): - URL_TEMPLATE = 'event/{event_id}/ra/' - - _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]") - _power_mic_selector = (By.CSS_SELECTOR, ".bootstrap-select") - - form_items = { - 'nonstandard_equipment': (regions.RadioSelect, (By.ID, 'id_nonstandard_equipment')), - 'nonstandard_use': (regions.RadioSelect, (By.ID, 'id_nonstandard_use')), - 'contractors': (regions.RadioSelect, (By.ID, 'id_contractors')), - 'other_companies': (regions.RadioSelect, (By.ID, 'id_other_companies')), - 'crew_fatigue': (regions.RadioSelect, (By.ID, 'id_crew_fatigue')), - 'general_notes': (regions.TextBox, (By.ID, 'id_general_notes')), - 'big_power': (regions.RadioSelect, (By.ID, 'id_big_power')), - 'outside': (regions.RadioSelect, (By.ID, 'id_outside')), - 'generators': (regions.RadioSelect, (By.ID, 'id_generators')), - 'other_companies_power': (regions.RadioSelect, (By.ID, 'id_other_companies_power')), - 'nonstandard_equipment_power': (regions.RadioSelect, (By.ID, 'id_nonstandard_equipment_power')), - 'multiple_electrical_environments': (regions.RadioSelect, (By.ID, 'id_multiple_electrical_environments')), - 'power_notes': (regions.TextBox, (By.ID, 'id_power_notes')), - 'noise_monitoring': (regions.RadioSelect, (By.ID, 'id_noise_monitoring')), - 'sound_notes': (regions.TextBox, (By.ID, 'id_sound_notes')), - 'known_venue': (regions.RadioSelect, (By.ID, 'id_known_venue')), - 'safe_loading': (regions.RadioSelect, (By.ID, 'id_safe_loading')), - 'safe_storage': (regions.RadioSelect, (By.ID, 'id_safe_storage')), - 'area_outside_of_control': (regions.RadioSelect, (By.ID, 'id_area_outside_of_control')), - 'barrier_required': (regions.RadioSelect, (By.ID, 'id_barrier_required')), - 'nonstandard_emergency_procedure': (regions.RadioSelect, (By.ID, 'id_nonstandard_emergency_procedure')), - 'special_structures': (regions.RadioSelect, (By.ID, 'id_special_structures')), - 'persons_responsible_structures': (regions.TextBox, (By.ID, 'id_persons_responsible_structures')), - 'suspended_structures': (regions.RadioSelect, (By.ID, 'id_suspended_structures')), - 'supervisor_consulted': (regions.CheckBox, (By.ID, 'id_supervisor_consulted')), - 'rigging_plan': (regions.TextBox, (By.ID, 'id_rigging_plan')), - 'parking_and_access': (regions.RadioSelect, (By.ID, 'id_parking_and_access')), - } - - @property - def power_mic(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector)) - - @property - def success(self): - return '/event/ra' in self.driver.current_url - - -class EditRiskAssessment(CreateRiskAssessment): - URL_TEMPLATE = 'event/ra/{pk}/edit' - - @property - def success(self): - return '/edit' not in self.driver.current_url - - -class CreateEventChecklist(FormPage): - URL_TEMPLATE = 'event/{event_id}/checklist' - - _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]") - - form_items = { - 'safe_parking': (regions.CheckBox, (By.ID, 'id_safe_parking')), - 'safe_packing': (regions.CheckBox, (By.ID, 'id_safe_packing')), - 'exits': (regions.CheckBox, (By.ID, 'id_exits')), - 'trip_hazard': (regions.CheckBox, (By.ID, 'id_trip_hazard')), - 'warning_signs': (regions.CheckBox, (By.ID, 'id_warning_signs')), - 'ear_plugs': (regions.CheckBox, (By.ID, 'id_ear_plugs')), - 'hs_location': (regions.TextBox, (By.ID, 'id_hs_location')), - 'extinguishers_location': (regions.TextBox, (By.ID, 'id_extinguishers_location')), - } - - @property - def success(self): - return '{event_id}' not in self.driver.current_url - - -class CreatePowerTestRecord(FormPage): - URL_TEMPLATE = 'event/{event_id}/power' - - _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Save')]") - _power_mic_selector = (By.XPATH, "//div[select[@id='id_power_mic']]") - - form_items = { - 'rcds': (regions.CheckBox, (By.ID, 'id_rcds')), - 'supply_test': (regions.CheckBox, (By.ID, 'id_supply_test')), - 'earthing': (regions.CheckBox, (By.ID, 'id_earthing')), - 'pat': (regions.CheckBox, (By.ID, 'id_pat')), - 'source_rcd': (regions.CheckBox, (By.ID, 'id_source_rcd')), - 'labelling': (regions.CheckBox, (By.ID, 'id_labelling')), - 'fd_voltage_l1': (regions.TextBox, (By.ID, 'id_fd_voltage_l1')), - 'fd_voltage_l2': (regions.TextBox, (By.ID, 'id_fd_voltage_l2')), - 'fd_voltage_l3': (regions.TextBox, (By.ID, 'id_fd_voltage_l3')), - 'fd_phase_rotation': (regions.CheckBox, (By.ID, 'id_fd_phase_rotation')), - 'fd_earth_fault': (regions.TextBox, (By.ID, 'id_fd_earth_fault')), - 'fd_pssc': (regions.TextBox, (By.ID, 'id_fd_pssc')), - 'w1_description': (regions.TextBox, (By.ID, 'id_w1_description')), - 'w1_polarity': (regions.CheckBox, (By.ID, 'id_w1_polarity')), - 'w1_voltage': (regions.TextBox, (By.ID, 'id_w1_voltage')), - 'w1_earth_fault': (regions.TextBox, (By.ID, 'id_w1_earth_fault')), - } - - @property - def power_mic(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._power_mic_selector)) - - @property - def success(self): - return '{event_id}' not in self.driver.current_url - - -class EditEventChecklist(CreateEventChecklist): - URL_TEMPLATE = '/event/checklist/{pk}/edit' - - @property - def success(self): - return 'edit' not in self.driver.current_url - - -class GenericList(BasePage): - _search_selector = (By.CSS_SELECTOR, 'div.input-group:nth-child(2) > input:nth-child(1)') - _search_go_selector = (By.ID, 'id_search') - _add_item_selector = (By.CSS_SELECTOR, '.btn-success') - - -class UserPage(BasePage): - URL_TEMPLATE = 'user/' - - _api_key_selector = (By.ID, 'api-key') - _cal_url_selector = (By.ID, 'cal-url') - _generation_button_selector = (By.LINK_TEXT, 'Generate API Key') - - @property - def api_key(self): - return self.find_element(*self._api_key_selector).text - - @property - def cal_url(self): - return self.find_element(*self._cal_url_selector).text - - def toggle_filter(self, type_name): - self.find_element(By.XPATH, "//input[@value='" + type_name + "']").click() - - def generate_key(self): - self.find_element(*self._generation_button_selector).click() - - -class CalendarPage(BasePage): - URL_TEMPLATE = 'rigboard/calendar' - - _go_locator = (By.ID, 'go-to-date-button') - - _today_selector = (By.ID, 'today-button') - - _prev_selector = (By.ID, 'prev-button') - _next_selector = (By.ID, 'next-button') - - _month_selector = (By.ID, 'month-button') - _week_selector = (By.ID, 'week-button') - _day_selector = (By.ID, 'day-button') - - def go(self): - return self.find_element(*self._go_locator).click() - - @property - def target_date(self): - return regions.DatePicker(self, self.find_element(By.ID, 'go-to-date-input')) - - def today(self): - return self.find_element(*self._today_selector).click() - - def prev(self): - return self.find_element(*self._prev_selector).click() - - def next(self): - return self.find_element(*self._next_selector).click() - - def month(self): - return self.find_element(*self._month_selector).click() - - def week(self): - return self.find_element(*self._week_selector).click() - - def day(self): - return self.find_element(*self._day_selector).click() diff --git a/RIGS/tests/regions.py b/RIGS/tests/regions.py deleted file mode 100644 index 3ee4d7d2..00000000 --- a/RIGS/tests/regions.py +++ /dev/null @@ -1,48 +0,0 @@ -from pypom import Region -from selenium.webdriver.common.by import By - -from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea - - -class Header(Region): - def find_link(self, link_text): - return self.driver.find_element(By.PARTIAL_LINK_TEXT, link_text) - - -class ItemRow(Region): - _name_locator = (By.XPATH, '//span[@class="name"]') - _description_locator = (By.XPATH, '//div[@class="item-description"]') - _price_locator = (By.XPATH, '//span[@class="cost"]') - _quantity_locator = (By.XPATH, '//td[@class="quantity"]') - _subtotal_locator = (By.XPATH, '//span[@class="sub-total"]') - - @property - def name(self): - return self.find_element(*self._name_locator).text - - @property - def description(self): - return self.find_element(*self._description_locator).text - - @property - def price(self): - return self.find_element(*self._price_locator).text - - @property - def quantity(self): - return self.find_element(*self._quantity_locator).text - - @property - def subtotal(self): - return self.find_element(*self._subtotal_locator).text - - -class ItemModal(Modal): - _header_selector = (By.TAG_NAME, 'h4') - - form_items = { - 'name': (TextBox, (By.ID, 'item_name')), - 'description': (SimpleMDETextArea, (By.ID, 'item_description')), - 'quantity': (TextBox, (By.ID, 'item_quantity')), - 'price': (TextBox, (By.ID, 'item_cost')) - } diff --git a/RIGS/tests/test_functional.py b/RIGS/tests/test_functional.py index d156a61a..3234612f 100644 --- a/RIGS/tests/test_functional.py +++ b/RIGS/tests/test_functional.py @@ -5,20 +5,18 @@ import pytest from django.conf import settings from django.core import mail, signing from django.http import HttpResponseBadRequest -from django.test import TestCase from django.urls import reverse -import PyRIGS.tests.base from RIGS import models from pytest_django.asserts import assertContains, assertNotContains, assertFormError def setup_event(): - venue = models.Venue.objects.create(name='Authorisation Test Venue') - client = models.Person.objects.create(name='Authorisation Test Person', email='authorisation@functional.test') - organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=True) + venue = models.Venue.objects.create(name="Authorisation Test Venue") + client = models.Person.objects.create(name="Authorisation Test Person", email="authorisation@functional.test") + organisation = models.Organisation.objects.create(name="Authorisation Test Organisation", union_account=True) return models.Event.objects.create( - name='Authorisation Test', + name="Authorisation Test", start_date=date.today(), venue=venue, person=client, @@ -30,35 +28,46 @@ def setup_mail(event, profile): profile.email = "teccie@nottinghamtec.co.uk" profile.save() auth_data = { - 'name': 'Test ABC', - 'po': '1234ABCZXY', - 'account_code': 'ABC TEST 12345', - 'uni_id': 1234567890, - 'tos': True + "name": "Test ABC", + "po": "1234ABCZXY", + "account_code": "ABC TEST 12345", + "uni_id": 1234567890, + "tos": True, } - hmac = signing.dumps({'pk': event.pk, 'email': 'authemail@function.test', - 'sent_by': profile.pk}) - url = reverse('event_authorise', kwargs={'pk': event.pk, 'hmac': hmac}) + hmac = signing.dumps({"pk": event.pk, "email": "authemail@function.test", "sent_by": profile.pk}) + url = reverse("event_authorise", kwargs={"pk": event.pk, "hmac": hmac}) return auth_data, hmac, url def test_create(admin_client): - url = reverse('event_create') + url = reverse("event_create") # end time before start access after start - response = admin_client.post(url, {'start_date': datetime.date(2020, 1, 1), 'start_time': datetime.time(10, 00), - 'end_time': datetime.time(9, 00), - 'access_at': datetime.datetime(2020, 1, 5, 10)}) - assertFormError(response, 'form', 'end_time', - "Unless you've invented time travel, the event can't finish before it has started.") - assertFormError(response, 'form', 'access_at', - "Regardless of what some clients might think, access time cannot be after the event has started.") + response = admin_client.post( + url, + { + "start_date": datetime.date(2020, 1, 1), + "start_time": datetime.time(10, 00), + "end_time": datetime.time(9, 00), + "access_at": datetime.datetime(2020, 1, 5, 10), + }, + ) + assertFormError( + response.context["form"], + "end_time", + "Unless you've invented time travel, the event can't finish before it has started.", + ) + assertFormError( + response.context["form"], + "access_at", + "Regardless of what some clients might think, access time cannot be after the event has started.", + ) def test_requires_valid_hmac(client, admin_user): event = setup_event() auth_data, hmac, url = setup_mail(event, admin_user) bad_hmac = hmac[:-1] - url = reverse('event_authorise', kwargs={'pk': event.pk, 'hmac': bad_hmac}) + url = reverse("event_authorise", kwargs={"pk": event.pk, "hmac": bad_hmac}) response = client.get(url) assert isinstance(response, HttpResponseBadRequest) # TODO: Add some form of sensible user facing error @@ -78,13 +87,13 @@ def test_validation(client, admin_user): response = client.post(url) assertContains(response, "This field is required.", 5) - auth_data['amount'] = event.total + 1 + auth_data["amount"] = event.total + 1 response = client.post(url, auth_data) assertContains(response, "The amount authorised must equal the total for the event") assertNotContains(response, "This field is required.") - auth_data['amount'] = event.total + auth_data["amount"] = event.total response = client.post(url, auth_data) assertContains(response, "Your event has been authorised") @@ -96,16 +105,17 @@ def test_validation(client, admin_user): def test_duplicate_warning(client, admin_user): event = setup_event() auth_data, hmac, url = setup_mail(event, admin_user) - auth = models.EventAuthorisation.objects.create(event=event, name='Test ABC', email='dupe@functional.test', - amount=event.total, sent_by=admin_user) + auth = models.EventAuthorisation.objects.create( + event=event, name="Test ABC", email="dupe@functional.test", amount=event.total, sent_by=admin_user + ) response = client.get(url) - assertContains(response, 'This event has already been authorised.') + assertContains(response, "This event has already been authorised.") auth.amount += 1 auth.save() response = client.get(url) - assertContains(response, 'amount has changed') + assertContains(response, "amount has changed") @pytest.mark.django_db @@ -114,44 +124,44 @@ def test_email_sent(admin_client, admin_user, mailoutbox): auth_data, hmac, url = setup_mail(event, admin_user) data = auth_data - data['amount'] = event.total + data["amount"] = event.total response = admin_client.post(url, data) assertContains(response, "Your event has been authorised.") assert len(mailoutbox) == 2 - assert mailoutbox[0].to == ['authemail@function.test'] + assert mailoutbox[0].to == ["authemail@function.test"] assert mailoutbox[1].to == [settings.AUTHORISATION_NOTIFICATION_ADDRESS] def test_email_check(admin_client, admin_user): event = setup_event() - url = reverse('event_authorise_request', kwargs={'pk': event.pk}) - admin_user.email = 'teccie@someotherdomain.com' + url = reverse("event_authorise_request", kwargs={"pk": event.pk}) + admin_user.email = "teccie@someotherdomain.com" admin_user.save() response = admin_client.post(url) - assertContains(response, 'must have an @nottinghamtec.co.uk email address') + assertContains(response, "must have an @nottinghamtec.co.uk email address") def test_request_send(admin_client, admin_user): event = setup_event() - url = reverse('event_authorise_request', kwargs={'pk': event.pk}) - admin_user.email = 'teccie@nottinghamtec.co.uk' + url = reverse("event_authorise_request", kwargs={"pk": event.pk}) + admin_user.email = "teccie@nottinghamtec.co.uk" admin_user.save() response = admin_client.post(url) - assertContains(response, 'This field is required.') + assertContains(response, "This field is required.") mail.outbox = [] - response = admin_client.post(url, {'email': 'client@functional.test'}) + response = admin_client.post(url, {"email": "client@functional.test"}) assert response.status_code == 302 assert len(mail.outbox) == 1 email = mail.outbox[0] - assert 'client@functional.test' in email.to - assert '/event/%d/' % event.pk in email.body + assert "client@functional.test" in email.to + assert "/event/%d/" % event.pk in email.body # Check sent by details are populated event.refresh_from_db() assert event.auth_request_by == admin_user - assert event.auth_request_to == 'client@functional.test' + assert event.auth_request_to == "client@functional.test" assert event.auth_request_at is not None diff --git a/RIGS/tests/test_interaction.py b/RIGS/tests/test_interaction.py deleted file mode 100644 index 85735dae..00000000 --- a/RIGS/tests/test_interaction.py +++ /dev/null @@ -1,780 +0,0 @@ -import datetime -from datetime import date, time, timedelta - -from django.test.client import Client -from django.utils import timezone -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.ui import WebDriverWait - -from PyRIGS.tests import base -from PyRIGS.tests import regions as base_regions -from PyRIGS.tests.base import (AutoLoginTest, screenshot_failure_cls) -from PyRIGS.tests.pages import animation_is_finished -from RIGS import models -from RIGS.tests import regions -from . import pages -import pytest -import time as t - - -pytestmark = pytest.mark.django_db(transaction=True) - - -@screenshot_failure_cls -class BaseRigboardTest(AutoLoginTest): - def setUp(self): - self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') - super().setUp() - self.client = models.Person.objects.create(name='Rigboard Test Person', email='rigboard@functional.test') - self.wait = WebDriverWait(self.driver, 10) - - def select_event_type(self, event_type): - self.wait.until(animation_is_finished()) - self.assertFalse(self.page.is_expanded) - self.page.select_event_type(event_type) - self.wait.until(animation_is_finished()) - self.assertTrue(self.page.is_expanded) - - -@screenshot_failure_cls -class TestRigboard(BaseRigboardTest): - def setUp(self): - super().setUp() - self.testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, - start_date=date.today() + timedelta(days=6), - description="start future no end", - purchase_order='TESTPO', - person=self.client, - auth_request_by=self.profile, - auth_request_at=base.create_datetime(2015, 0o6, 0o4, 10, 00), - auth_request_to="some@email.address") - - item1 = models.EventItem( - event=self.testEvent, - name="Test Item 1", - cost="10.00", - quantity="1", - order=1 - ).save() - item2 = models.EventItem( - event=self.testEvent, - name="Test Item 2", - description="Foo", - cost="9.72", - quantity="3", - order=2, - ).save() - self.testEvent2 = models.Event.objects.create(name="TE E2", status=models.Event.PROVISIONAL, - start_date=date.today() + timedelta(days=8), - description="start future no end, later", - purchase_order='TESTPO', - person=self.client, - auth_request_by=self.profile, - auth_request_at=base.create_datetime(2015, 0o6, 0o4, 10, 00), - auth_request_to="some@email.address") - self.page = pages.Rigboard(self.driver, self.live_server_url).open() - - def test_buttons(self): - header = regions.Header(self.page, self.driver.find_element(By.CSS_SELECTOR, '.navbar')) - # TODO Switch to checking reversed links (difficult because of arguments) - header.find_link("Rigboard").click() - self.assertEqual( - self.live_server_url + '/rigboard/', self.driver.current_url) - header.find_link("Archive").click() - self.assertEqual( - self.live_server_url + '/event/archive/', self.driver.current_url) - # TODO - This fails for some reason - # header.find_link("New").click() - # self.assertEqual( - # self.live_server_url + '/event/create/', self.driver.current_url) - - def test_event_order(self): - self.assertIn(self.testEvent.start_date.strftime('%-d %b %Y'), self.page.events[0].dates) - self.assertIn(self.testEvent2.start_date.strftime('%-d %b %Y'), self.page.events[1].dates) - - def test_add_button(self): - self.page.add() - self.assertIn('create', self.driver.current_url) - # Ideally get a response object to assert 200 on - - -@screenshot_failure_cls -class TestEventCreate(BaseRigboardTest): - def setUp(self): - super().setUp() - self.page = pages.CreateEvent(self.driver, self.live_server_url).open() - - def test_rig_creation(self): - self.select_event_type("Rig") - - self.page.person_selector.search(self.client.name) - self.page.person_selector.set_option(self.client.name, True) - self.page.person_selector.toggle() - self.assertFalse(self.page.person_selector.is_open) - - self.page.name = "Test Rig" - - # Both dates, no times, end before start - self.page.start_date = datetime.date(2020, 1, 10) - self.page.end_date = datetime.date(2020, 1, 1) - # Expected to fail - self.page.submit() - self.assertFalse(self.page.success) - self.assertIn("Unless you've invented time travel, the event can't finish before it has started.", self.page.errors["End date"]) - self.wait.until(animation_is_finished()) - - # Fix it - self.page.end_date = datetime.date(2020, 1, 11) - self.page.access_at = datetime.datetime(2020, 1, 8, 9) - self.page.dry_hire = True - self.page.status = "Booked" - self.page.collected_by = "Fred" - self.page.po = "1234" - self.page.notes = "A note!" - - self.page.submit() - self.assertTrue(self.page.success) - - # TODO - def test_modals(self): - self.select_event_type("Rig") - # Create new person - modal = self.page.add_person() - # animation_is_finished doesn't work for whatever reason... - self.wait.until(EC.visibility_of_element_located((By.ID, 'modal'))) - self.assertTrue(modal.is_open) - self.assertIn("Create Person", modal.header) - - # Fill person form out and submit - person_name = "Test Person" - modal.name = person_name - modal.submit() - self.wait.until(EC.invisibility_of_element_located((By.ID, 'modal'))) - self.assertFalse(modal.is_open) - - # See new person selected - self.page.person_selector.toggle() - self.assertEqual(self.page.person_selector.options[0].name, person_name) - self.page.person_selector.toggle() - - # Change mind and add another - self.wait.until(animation_is_finished()) - modal = self.page.add_person() - self.wait.until(EC.visibility_of_element_located((By.ID, 'modal'))) - self.assertTrue(modal.is_open) - self.assertIn("Create Person", modal.header) - - person_name = "Test Person 2" - modal.name = person_name - modal.submit() - self.wait.until(EC.invisibility_of_element_located((By.ID, 'modal'))) - self.assertFalse(modal.is_open) - self.page.person_selector.toggle() - self.assertEqual(self.page.person_selector.options[0].name, person_name) - self.page.person_selector.toggle() - - # TODO - - def test_event_item_creation(self): - self.select_event_type("Rig") - - self.page.name = "Test Event with Items" - - self.page.person_selector.search(self.client.name) - self.page.person_selector.set_option(self.client.name, True) - # TODO This should not be necessary, normally closes automatically - self.page.person_selector.toggle() - self.assertFalse(self.page.person_selector.is_open) - - # Note to self, don't set dates before 2014, which is the beginning of VAT as far as the tests are concerned... - self.page.start_date = datetime.date(2084, 1, 1) - - modal = self.page.add_event_item() - self.wait.until(animation_is_finished()) - # See modal has opened - self.assertTrue(modal.is_open) - self.assertIn("New Event", modal.header) - - modal.name = "Test Item 1" - modal.description = "This is an item description\nthat for reasons unknown spans two lines" - modal.quantity = "2" - modal.price = "23.95" - modal.submit() - self.wait.until(animation_is_finished()) - - # Confirm item has been saved to json field - objectitems = self.driver.execute_script("return objectitems;") - self.assertEqual(1, len(objectitems)) - testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID - self.assertEqual("Test Item 1", testitem['name']) - self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields - - total = self.driver.find_element(By.ID, 'total') - ActionChains(self.driver).move_to_element(total).perform() - - # See new item appear in table - row = self.page.item_row("-1") # ID number is known, see above - self.assertIn("Test Item 1", row.name) - self.assertIn("This is an item description", - row.description) - self.assertEqual('23.95', row.price) - self.assertEqual("2", row.quantity) - self.assertEqual('47.90', row.subtotal) - - # Check totals TODO convert to page properties - self.assertEqual("47.90", self.driver.find_element(By.ID, 'sumtotal').text) - self.assertIn("(TBC)", self.driver.find_element(By.ID, 'vat-rate').text) - self.assertEqual("9.58", self.driver.find_element(By.ID, 'vat').text) - self.assertEqual("57.48", total.text) - - self.page.submit() - - # TODO Testing of internal rig (approval system interface) - - def test_non_rig_creation(self): - self.select_event_type("Non-Rig") - - self.assertFalse(self.page.person_selector.is_open) - - rig_name = "Test Non-Rig" - self.page.name = rig_name - - # Double-check we don't lose data when swapping - self.page.select_event_type("Rig") - self.wait.until(animation_is_finished()) - self.assertEqual(self.page.name, rig_name) - self.wait.until(animation_is_finished()) - self.page.select_event_type("Non-Rig") - - self.page.start_date = datetime.date(2020, 1, 1) - self.page.status = "Confirmed" - - self.page.submit() - self.assertTrue(self.page.success) - - -@screenshot_failure_cls -class TestEventDuplicate(BaseRigboardTest): - def setUp(self): - super().setUp() - self.testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, - start_date=date.today() + timedelta(days=6), - description="start future no end", - purchase_order='TESTPO', - person=self.client, - auth_request_by=self.profile, - auth_request_at=base.create_datetime(2015, 0o6, 0o4, 10, 00), - auth_request_to="some@email.address") - - item1 = models.EventItem( - event=self.testEvent, - name="Test Item 1", - cost="10.00", - quantity="1", - order=1 - ).save() - item2 = models.EventItem( - event=self.testEvent, - name="Test Item 2", - description="Foo", - cost="9.72", - quantity="3", - order=2, - ).save() - self.page = pages.DuplicateEvent(self.driver, self.live_server_url, event_id=self.testEvent.pk).open() - - def test_rig_duplicate(self): - table = self.page.item_table - self.assertIn("Test Item 1", table.text) - self.assertIn("Test Item 2", table.text) - - # Check the info message is visible - self.assertIn("Event data duplicated but not yet saved", self.page.warning) - - modal = self.page.add_event_item() - self.wait.until(animation_is_finished()) - # See modal has opened - self.assertTrue(modal.is_open) - self.assertIn(self.testEvent.name, modal.header) - - modal.name = "Test Item 3" - modal.description = "This is an item description\nthat for reasons unknown spans two lines" - modal.quantity = "2" - modal.price = "23.95" - modal.submit() - self.wait.until(animation_is_finished()) - - # Attempt to save - self.page.submit() - - # TODO Rewrite when EventDetail page is implemented - newEvent = models.Event.objects.latest('pk') - - assert newEvent.auth_request_to == '' - self.assertEqual(newEvent.auth_request_by, None) - self.assertEqual(newEvent.auth_request_at, None) - - self.assertFalse(newEvent.authorised) - - self.assertNotIn("N%05d" % self.testEvent.pk, self.driver.find_element(By.XPATH, '//h2').text) - self.assertNotIn("Event data duplicated but not yet saved", self.page.warning) # Check info message not visible - - # Check the new items are visible - table = self.page.item_table - self.assertIn("Test Item 1", table.text) - self.assertIn("Test Item 2", table.text) - self.assertIn("Test Item 3", table.text) - - infoPanel = self.driver.find_element(By.XPATH, '//div[contains(text(), "Event Info")]/..') - self.assertIn("N%05d" % self.testEvent.pk, infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text) - # Check the PO hasn't carried through - self.assertNotIn("TESTPO", infoPanel.find_element(By.XPATH, '//dt[text()="PO"]/following-sibling::dd[1]').text) - - self.assertIn("N%05d" % self.testEvent.pk, - infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text) - - self.driver.get(self.live_server_url + '/event/' + str(self.testEvent.pk)) # Go back to the old event - - # Check that based-on hasn't crept into the old event - infoPanel = self.driver.find_element(By.XPATH, '//div[contains(text(), "Event Info")]/..') - self.assertNotIn("N%05d" % self.testEvent.pk, - infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text) - # Check the PO remains on the old event - self.assertIn("TESTPO", infoPanel.find_element(By.XPATH, '//dt[text()="PO"]/following-sibling::dd[1]').text) - - self.assertNotIn("N%05d" % self.testEvent.pk, - infoPanel.find_element(By.XPATH, '//dt[text()="Based On"]/following-sibling::dd[1]').text) - - # Check the items are as they were - table = self.page.item_table # ID number is known, see above - self.assertIn("Test Item 1", table.text) - self.assertIn("Test Item 2", table.text) - self.assertNotIn("Test Item 3", table.text) - - -@screenshot_failure_cls -class TestEventEdit(BaseRigboardTest): - def setUp(self): - super().setUp() - self.testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, - start_date=date.today() + timedelta(days=6), - description="start future no end", - purchase_order='TESTPO', - person=self.client, - auth_request_by=self.profile, - auth_request_at=base.create_datetime(2015, 0o6, 0o4, 10, 00), - auth_request_to="some@email.address") - - item1 = models.EventItem( - event=self.testEvent, - name="Test Item 1", - cost="10.00", - quantity="1", - order=1 - ).save() - item2 = models.EventItem( - event=self.testEvent, - name="Test Item 2", - description="Foo", - cost="9.72", - quantity="3", - order=2, - ).save() - self.page = pages.EditEvent(self.driver, self.live_server_url, event_id=self.testEvent.pk).open() - - def test_rig_edit(self): - self.page.name = "Edited Event" - - modal = self.page.add_event_item() - self.wait.until(animation_is_finished()) - # See modal has opened - self.assertTrue(modal.is_open) - self.assertIn(self.testEvent.name, modal.header) - - modal.name = "Test Item 3" - modal.description = "This is an item description\nthat for reasons unknown spans two lines" - modal.quantity = "2" - modal.price = "23.95" - modal.submit() - self.wait.until(animation_is_finished()) - - # Attempt to save - ActionChains(self.driver).move_to_element(self.page.item_table).perform() - self.page.submit() - - self.assertTrue(self.page.success) - - self.page = pages.EventDetail(self.driver, self.live_server_url, event_id=self.testEvent.pk).open() - self.testEvent.refresh_from_db() - self.assertIn(self.testEvent.name, self.page.event_name) - self.assertEqual(self.page.name, self.testEvent.person.name) - # Check the new items are visible - table = self.page.item_table - self.assertIn("Test Item 3", table.text) - - -@screenshot_failure_cls -class TestEventDetail(BaseRigboardTest): - def setUp(self): - super().setUp() - self.testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, - start_date=date.today() + timedelta(days=6), - description="start future no end", - purchase_order='TESTPO', - person=self.client, - auth_request_by=self.profile, - auth_request_at=base.create_datetime(2015, 0o6, 0o4, 10, 00), - auth_request_to="some@email.address") - - item1 = models.EventItem( - event=self.testEvent, - name="Test Item 1", - cost="10.00", - quantity="1", - order=1 - ).save() - item2 = models.EventItem( - event=self.testEvent, - name="Test Item 2", - description="Foo", - cost="9.72", - quantity="3", - order=2, - ).save() - self.page = pages.EventDetail(self.driver, self.live_server_url, event_id=self.testEvent.pk).open() - - def test_rig_detail(self): - self.assertIn("N%05d | %s" % (self.testEvent.pk, self.testEvent.name), self.page.event_name) - self.assertEqual(self.client.name, self.page.name) - self.assertEqual(self.client.email, self.page.email) - assert self.client.phone == '' - - -@screenshot_failure_cls -class TestCalendar(BaseRigboardTest): - def setUp(self): - super().setUp() - self.all_events = set(range(1, 18)) - self.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18) - self.not_current_events = set(self.all_events) - set(self.current_events) - - # produce 7 normal events - 5 current - 1 last week - 1 two years ago - 2 provisional - 2 confirmed - 3 booked - models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, - start_date=date.today() + timedelta(days=6), description="start future no end") - models.Event.objects.create(name="TE E2", status=models.Event.PROVISIONAL, start_date=date.today(), - description="start today no end") - models.Event.objects.create(name="TE E3", status=models.Event.CONFIRMED, start_date=date.today(), - end_date=date.today(), description="start today with end today") - models.Event.objects.create(name="TE E4", status=models.Event.CONFIRMED, - start_date=date.today() - timedelta(weeks=104), - description="start past 2 years no end") - models.Event.objects.create(name="TE E5", status=models.Event.BOOKED, - start_date=date.today() - timedelta(days=7), - end_date=date.today() - timedelta(days=1), - description="start past 1 week with end past") - models.Event.objects.create(name="TE E6", status=models.Event.BOOKED, - start_date=date.today() - timedelta(days=2), - start_time=time(8, 00), - end_date=date.today() + timedelta(days=2), - end_time=time(23, 00), description="start past, end future") - models.Event.objects.create(name="TE E7", status=models.Event.BOOKED, - start_date=date.today() + timedelta(days=2), - end_date=date.today() + timedelta(days=2), description="start + end in future") - - # 2 cancelled - 1 current - models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2), - end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED, - description="cancelled in future") - models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1), - end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED, - description="cancelled and started") - - # 5 dry hire - 3 current - 1 cancelled - models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, description="dryhire today") - models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, checked_in_by=self.profile, - description="dryhire today, checked in") - models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True, - status=models.Event.BOOKED, description="dryhire past") - models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True, - checked_in_by=self.profile, description="dryhire past checked in") - models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True, - status=models.Event.CANCELLED, description="dryhire today cancelled") - - # 4 non rig - 3 current - models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False, description="non rig today") - models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False, - description="non rig tomorrow") - models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False, - description="non rig yesterday") - models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED, - description="non rig today cancelled") - - self.page = pages.UserPage(self.driver, self.live_server_url).open() - - def test_api_key_generation(self): - # Completes and comes back to /user/ - # Checks that no api key is displayed - self.assertEqual("No API Key Generated", self.page.api_key) - - # Now creates an API key, and check a URL is displayed one - self.page.generate_key() - self.assertIn("rigs.ics", self.page.cal_url) - self.assertNotIn("?", self.page.cal_url) - - # Lets change everything so it's not the default value - self.page.toggle_filter('rig') - self.page.toggle_filter('non-rig') - self.page.toggle_filter('dry-hire') - self.page.toggle_filter('cancelled') - self.page.toggle_filter('provisional') - self.page.toggle_filter('confirmed') - self.page.toggle_filter('only_mic') - - # and then check the url is correct - self.assertIn( - "rigs.ics?rig=false&non-rig=false&dry-hire=false&cancelled=true&provisional=false&confirmed=false&only_mic=true", - self.page.cal_url) - - # Awesome - all seems to work - - def test_ics_files(self): - specialEvent = models.Event.objects.get(name="TE E6") - - # Now creates an API key, and check a URL is displayed one - self.page.generate_key() - - c = Client() - - # Default settings - should have all non-cancelled events - # Get the ical file (can't do this in selanium because reasons) - icalUrl = self.page.cal_url - response = c.get(self.page.cal_url) - self.assertEqual(200, response.status_code) - - # content = response.content.decode('utf-8') - - # Check has entire file - self.assertContains(response, "BEGIN:VCALENDAR") - self.assertContains(response, "END:VCALENDAR") - - expectedIn = [1, 2, 3, 5, 6, 7, 10, 11, 12, 13, 15, 16, 17] - for test in range(1, 18): - if test in expectedIn: - self.assertContains(response, "TE E" + str(test) + " ") - else: - self.assertNotContains(response, "TE E" + str(test) + " ") - - # Check that times have been included correctly - self.assertContains(response, - specialEvent.start_date.strftime('%Y%m%d') + 'T' + specialEvent.start_time.strftime( - '%H%M%S')) - self.assertContains(response, - specialEvent.end_date.strftime('%Y%m%d')) - self.assertContains(response, - specialEvent.end_time.strftime('%H%M%S')) - - # Only non rigs - self.page.toggle_filter('rig') - self.page.toggle_filter('non-rig') - - icalUrl = self.page.cal_url - response = c.get(icalUrl) - self.assertEqual(200, response.status_code) - - expectedIn = [10, 11, 12, 13] - for test in range(1, 18): - if test in expectedIn: - self.assertContains(response, "TE E" + str(test) + " ") - else: - self.assertNotContains(response, "TE E" + str(test) + " ") - - # Only provisional rigs - self.page.toggle_filter('rig') - self.page.toggle_filter('dry-hire') - self.page.toggle_filter('confirmed') - - icalUrl = self.page.cal_url - response = c.get(icalUrl) - self.assertEqual(200, response.status_code) - - expectedIn = [1, 2] - for test in range(1, 18): - if test in expectedIn: - self.assertContains(response, "TE E" + str(test) + " ") - else: - self.assertNotContains(response, "TE E" + str(test) + " ") - - # Only cancelled non-rigs - self.page.toggle_filter('rig') - self.page.toggle_filter('non-rig') - self.page.toggle_filter('provisional') - self.page.toggle_filter('cancelled') - - icalUrl = self.page.cal_url - response = c.get(icalUrl) - self.assertEqual(200, response.status_code) - - expectedIn = [18] - for test in range(1, 18): - if test in expectedIn: - self.assertContains(response, "TE E" + str(test) + " ") - else: - self.assertNotContains(response, "TE E" + str(test) + " ") - - # Nothing selected - self.page.toggle_filter('non-rig') - self.page.toggle_filter('cancelled') - - icalUrl = self.page.cal_url - response = c.get(icalUrl) - self.assertEqual(200, response.status_code) - - expectedIn = [] - for test in range(1, 18): - if test in expectedIn: - self.assertContains(response, "TE E" + str(test) + " ") - else: - self.assertNotContains(response, "TE E" + str(test) + " ") - - -def test_calendar_buttons(logged_in_browser, live_server): # If FullCalendar fails to load for whatever reason, the buttons don't work - page = pages.CalendarPage(logged_in_browser.driver, live_server.url).open() - assert timezone.now().strftime("%Y-%m") in logged_in_browser.url - - target_date = datetime.date(2020, 1, 1) - page.target_date.set_value(target_date) - page.go() - assert page.target_date.value.strftime("%Y-%m") in logged_in_browser.url - - page.next() - target_date += datetime.timedelta(days=32) - assert target_date.strftime("%m") in logged_in_browser.url - - -def test_ra_edit(logged_in_browser, live_server, ra): - page = pages.EditRiskAssessment(logged_in_browser.driver, live_server.url, pk=ra.pk).open() - page.nonstandard_equipment = nse = True - page.general_notes = gn = "There are some notes, but I've not written them here as that would be helpful" - page.submit() - assert not page.success - page.supervisor_consulted = True - page.submit() - assert page.success - # Check that data is right - ra = models.RiskAssessment.objects.get(pk=ra.pk) - assert ra.general_notes == gn - assert ra.nonstandard_equipment == nse - - -def small_ec(page, admin_user): - page.safe_parking = True - page.safe_packing = True - page.exits = True - page.trip_hazard = True - page.warning_signs = True - page.ear_plugs = True - page.hs_location = "The Moon" - page.extinguishers_location = "With the rest of the fire" - - -def test_ec_create_small(logged_in_browser, live_server, admin_user, ra): - page = pages.CreateEventChecklist(logged_in_browser.driver, live_server.url, event_id=ra.event.pk).open() - small_ec(page, admin_user) - page.submit() - assert page.success - - -def test_ec_create_medium(logged_in_browser, live_server, admin_user, medium_ra): - page = pages.CreateEventChecklist(logged_in_browser.driver, live_server.url, event_id=medium_ra.event.pk).open() - - page.safe_parking = True - page.safe_packing = True - page.exits = True - page.trip_hazard = True - page.warning_signs = True - page.ear_plugs = True - page.hs_location = "Death Valley" - page.extinguishers_location = "With the rest of the fire" - - # Gotta scroll to make the button clickable - logged_in_browser.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") - page.submit() - assert page.success - - -def test_power_checklist(logged_in_browser, live_server, admin_user, power_test, medium_ra): - page = pages.CreatePowerTestRecord(logged_in_browser.driver, live_server.url, event_id=medium_ra.event.pk).open() - page.earthing = True - page.pat = True - page.source_rcd = True - page.labelling = True - page.fd_voltage_l1 = 240 - page.fd_voltage_l2 = 235 - page.fd_voltage_l3 = 0 - page.fd_phase_rotation = True - page.fd_earth_fault = "1.21" - page.fd_pssc = 1984 - page.w1_description = "In the carpark, by the bins" - page.w1_polarity = True - page.w1_voltage = 240 - page.w1_earth_fault = "0.42" - # If we do this first the search fails, for ... reasons - page.power_mic.search(admin_user.name) - page.power_mic.toggle() - assert not page.power_mic.is_open - - page.submit() - assert page.success - - -# TODO Can I loop through all the boolean fields and test them at once? -def test_ra_creation(logged_in_browser, live_server, admin_user, basic_event): - page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=basic_event.pk).open() - - # Check there are no defaults - assert page.nonstandard_equipment is None - - # No database side validation, only HTML5. - page.nonstandard_equipment = False - page.nonstandard_use = False - page.contractors = False - page.other_companies = False - page.crew_fatigue = False - page.general_notes = "There are no notes." - page.big_power = False - page.outside = False - page.power_mic.search(admin_user.first_name) - page.generators = False - page.other_companies_power = False - page.nonstandard_equipment_power = False - page.multiple_electrical_environments = False - page.power_notes = "Remember to bring some power" - page.noise_monitoring = False - page.sound_notes = "Loud, but not too loud" - page.known_venue = False - page.safe_loading = False - page.safe_storage = False - page.area_outside_of_control = False - page.barrier_required = False - page.nonstandard_emergency_procedure = False - page.special_structures = False - page.parking_and_access = False - # self.page.persons_responsible_structures = "Nobody and her cat, She" - - page.suspended_structures = True - # TODO Test for this proper - page.rigging_plan = "https://nottinghamtec.sharepoint.com/test/" - page.submit() - assert not page.success - - page.suspended_structures = False - page.submit() - assert page.success - - -def test_ra_no_duplicates(logged_in_browser, live_server, ra): - # Test that we can't make another one - page = pages.CreateRiskAssessment(logged_in_browser.driver, live_server.url, event_id=ra.event.pk).open() - assert 'edit' in logged_in_browser.url diff --git a/RIGS/tests/test_models.py b/RIGS/tests/test_models.py index 1450ffa6..c289cd18 100644 --- a/RIGS/tests/test_models.py +++ b/RIGS/tests/test_models.py @@ -1,5 +1,5 @@ from datetime import date, timedelta, datetime, time -from decimal import * +from decimal import Decimal import pytz import pytest @@ -15,18 +15,18 @@ def assert_decimal_equality(d1, d2): def test_str(): - profile = models.Profile(first_name='Test', last_name='Case') - assert str(profile) == 'Test Case' - profile.initials = 'TC' + profile = models.Profile(first_name="Test", last_name="Case") + assert str(profile) == "Test Case" + profile.initials = "TC" assert str(profile) == 'Test Case "TC"' @pytest.mark.django_db def test_find_correct(vat_rate): - new_rate = models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2') - r = models.VatRate.objects.find_rate('2015-03-01') + new_rate = models.VatRate.objects.create(start_at="2016-03-01", rate=0.15, comment="test2") + r = models.VatRate.objects.find_rate("2015-03-01") assert_decimal_equality(r.rate, vat_rate.rate) - r = models.VatRate.objects.find_rate('2016-03-01') + r = models.VatRate.objects.find_rate("2016-03-01") assert_decimal_equality(r.rate, new_rate.rate) @@ -38,7 +38,7 @@ def test_related_vatrate(basic_event, vat_rate): assert_decimal_equality(vat_rate.rate, basic_event.vat_rate.rate) -class EventTest(): +class EventTest: def test_count(many_events): # Sanity check we have the expected events created assert models.Event.objects.count() == 18 @@ -48,7 +48,6 @@ class EventTest(): assert models.Event.objects.rig_count() == 7 def test_current_events(many_events): - all_events = set(range(1, 18)) current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18) not_current_events = set(cls.all_events) - set(cls.current_events) current_events = models.Event.objects.current_events() @@ -65,7 +64,7 @@ class EventTest(): e1 = [] e2 = [] - for (key, event) in self.events.items(): + for key, event in self.events.items(): if event.pk % 2: event.venue = v1 e1.append(event) @@ -86,7 +85,7 @@ class EventTest(): e1 = [] e2 = [] - for (key, event) in self.events.items(): + for key, event in self.events.items(): if event.pk % 2: event.person = p1 e1.append(event) @@ -107,7 +106,7 @@ class EventTest(): e1 = [] e2 = [] - for (key, event) in self.events.items(): + for key, event in self.events.items(): if event.pk % 2: event.organisation = o1 e1.append(event) @@ -119,7 +118,7 @@ class EventTest(): assert set(e1) == set(o1.latest_events) assert set(e1) == set(o2.latest_events) - for (key, event) in self.events.items(): + for key, event in self.events.items(): event.organisation = None def test_organisation_person_join(many_events): @@ -219,23 +218,18 @@ def test_latest_time(): def test_in_bounds(): manager = models.Event.objects events = [ - manager.create(name="TE IB0", start_date='2016-01-02'), # yes no - manager.create(name="TE IB1", start_date='2015-12-31', end_date='2016-01-04'), - + manager.create(name="TE IB0", start_date="2016-01-02"), # yes no + manager.create(name="TE IB1", start_date="2015-12-31", end_date="2016-01-04"), # basic checks - manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'), - manager.create(name='TE IB3', start_date='2015-12-31', end_date='2016-01-03'), - manager.create(name='TE IB4', start_date='2016-01-04', - access_at=create_datetime(2016, 0o1, 0o3, 00, 00)), - manager.create(name='TE IB5', start_date='2016-01-04', - meet_at=create_datetime(2016, 0o1, 0o2, 00, 00)), - + manager.create(name="TE IB2", start_date="2016-01-02", end_date="2016-01-04"), + manager.create(name="TE IB3", start_date="2015-12-31", end_date="2016-01-03"), + manager.create(name="TE IB4", start_date="2016-01-04", access_at=create_datetime(2016, 0o1, 0o3, 00, 00)), + manager.create(name="TE IB5", start_date="2016-01-04", meet_at=create_datetime(2016, 0o1, 0o2, 00, 00)), # negative check - manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'), + manager.create(name="TE IB6", start_date="2015-12-31", end_date="2016-01-01"), ] - in_bounds = manager.events_in_bounds(create_datetime(2016, 1, 2, 0, 0), - create_datetime(2016, 1, 3, 0, 0)) + in_bounds = manager.events_in_bounds(create_datetime(2016, 1, 2, 0, 0), create_datetime(2016, 1, 3, 0, 0)) assert events[0] in in_bounds assert events[1], in_bounds assert events[2], in_bounds @@ -293,43 +287,51 @@ class EventPricingTestCase(TestCase): # Decimal type is needed here as that is what is returned from the model. # Using anything else results in a failure due to floating point arritmetic def test_sum_totals(self): - self.assertEqual(self.e1.sum_total, Decimal('70.40')) - self.assertEqual(self.e2.sum_total, Decimal('381.20')) + self.assertEqual(self.e1.sum_total, Decimal("70.40")) + self.assertEqual(self.e2.sum_total, Decimal("381.20")) def test_vat_rate(self): - self.assertEqual(self.e1.vat_rate.rate, Decimal('0.20')) - self.assertEqual(self.e2.vat_rate.rate, Decimal('0.10')) + self.assertEqual(self.e1.vat_rate.rate, Decimal("0.20")) + self.assertEqual(self.e2.vat_rate.rate, Decimal("0.10")) def test_vat_ammount(self): - self.assertEqual(self.e1.vat, Decimal('14.08')) - self.assertEqual(self.e2.vat, Decimal('38.12')) + self.assertEqual(self.e1.vat, Decimal("14.08")) + self.assertEqual(self.e2.vat, Decimal("38.12")) def test_grand_total(self): - self.assertEqual(self.e1.total, Decimal('84.48')) - self.assertEqual(self.e2.total, Decimal('419.32')) + self.assertEqual(self.e1.total, Decimal("84.48")) + self.assertEqual(self.e2.total, Decimal("419.32")) class EventAuthorisationTestCase(TestCase): def setUp(self): self.profile = models.Profile.objects.get_or_create( - first_name='Test', - last_name='TEC User', - username='eventauthtest', - email='teccie@functional.test', - is_superuser=True # lazily grant all permissions + first_name="Test", + last_name="TEC User", + username="eventauthtest", + email="teccie@functional.test", + is_superuser=True, # lazily grant all permissions )[0] - self.person = models.Person.objects.create(name='Authorisation Test Person') - self.organisation = models.Organisation.objects.create(name='Authorisation Test Organisation', union_account=True) - self.event = models.Event.objects.create(name="AuthorisationTestCase", person=self.person, organisation=self.organisation, - start_date=date.today()) + self.person = models.Person.objects.create(name="Authorisation Test Person") + self.organisation = models.Organisation.objects.create( + name="Authorisation Test Organisation", union_account=True + ) + self.event = models.Event.objects.create( + name="AuthorisationTestCase", person=self.person, organisation=self.organisation, start_date=date.today() + ) # Add some items - models.EventItem.objects.create(event=self.event, name="Authorisation test item", quantity=2, cost=123.45, - order=1) + models.EventItem.objects.create( + event=self.event, name="Authorisation test item", quantity=2, cost=123.45, order=1 + ) def test_event_property(self): - auth1 = models.EventAuthorisation.objects.create(event=self.event, email="authorisation@model.test.case", - name="Test Auth 1", amount=self.event.total - 1, - sent_by=self.profile) + auth1 = models.EventAuthorisation.objects.create( + event=self.event, + email="authorisation@model.test.case", + name="Test Auth 1", + amount=self.event.total - 1, + sent_by=self.profile, + ) self.assertFalse(self.event.authorised) auth1.amount = self.event.total auth1.save() @@ -337,7 +339,11 @@ class EventAuthorisationTestCase(TestCase): def test_last_edited(self): with reversion.create_revision(): - auth = models.EventAuthorisation.objects.create(event=self.event, email="authorisation@model.test.case", - name="Test Auth", amount=self.event.total, - sent_by=self.profile) + auth = models.EventAuthorisation.objects.create( + event=self.event, + email="authorisation@model.test.case", + name="Test Auth", + amount=self.event.total, + sent_by=self.profile, + ) self.assertIsNotNone(auth.last_edited_at) diff --git a/RIGS/tests/test_unit.py b/RIGS/tests/test_unit.py index 2426a59a..fd3188f9 100644 --- a/RIGS/tests/test_unit.py +++ b/RIGS/tests/test_unit.py @@ -5,7 +5,6 @@ from datetime import date from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.test import TestCase -from django.test.utils import override_settings from django.utils.safestring import SafeText from RIGS.templatetags.markdown_tags import markdown_filter from django.urls import reverse, reverse_lazy @@ -13,7 +12,8 @@ from django.utils import timezone from pytest_django.asserts import assertRedirects, assertNotContains, assertContains from PyRIGS.tests.base import assert_times_almost_equal, assert_oembed, login -from RIGS import models +from django.forms.models import model_to_dict +from RIGS import forms, models pytestmark = pytest.mark.django_db @@ -21,8 +21,9 @@ pytestmark = pytest.mark.django_db class TestAdminMergeObjects(TestCase): @classmethod def setUpTestData(cls): - cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, - is_active=True, is_staff=True) + cls.profile = models.Profile.objects.create( + username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True + ) cls.persons = { 1: models.Person.objects.create(name="Person 1"), 2: models.Person.objects.create(name="Person 2"), @@ -42,28 +43,44 @@ class TestAdminMergeObjects(TestCase): } cls.events = { - 1: models.Event.objects.create(name="TE E1", start_date=date.today(), person=cls.persons[1], - organisation=cls.organisations[3], venue=cls.venues[2]), - 2: models.Event.objects.create(name="TE E2", start_date=date.today(), person=cls.persons[2], - organisation=cls.organisations[2], venue=cls.venues[3]), - 3: models.Event.objects.create(name="TE E3", start_date=date.today(), person=cls.persons[3], - organisation=cls.organisations[1], venue=cls.venues[1]), - 4: models.Event.objects.create(name="TE E4", start_date=date.today(), person=cls.persons[3], - organisation=cls.organisations[3], venue=cls.venues[3]), + 1: models.Event.objects.create( + name="TE E1", + start_date=date.today(), + person=cls.persons[1], + organisation=cls.organisations[3], + venue=cls.venues[2], + ), + 2: models.Event.objects.create( + name="TE E2", + start_date=date.today(), + person=cls.persons[2], + organisation=cls.organisations[2], + venue=cls.venues[3], + ), + 3: models.Event.objects.create( + name="TE E3", + start_date=date.today(), + person=cls.persons[3], + organisation=cls.organisations[1], + venue=cls.venues[1], + ), + 4: models.Event.objects.create( + name="TE E4", + start_date=date.today(), + person=cls.persons[3], + organisation=cls.organisations[3], + venue=cls.venues[3], + ), } def setUp(self): - self.profile.set_password('testuser') + self.profile.set_password("testuser") self.profile.save() - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + self.assertTrue(self.client.login(username=self.profile.username, password="testuser")) def test_merge_confirmation(self): - change_url = reverse('admin:RIGS_venue_changelist') - data = { - 'action': 'merge', - '_selected_action': [str(val.pk) for key, val in self.venues.items()] - - } + change_url = reverse("admin:RIGS_venue_changelist") + data = {"action": "merge", "_selected_action": [str(val.pk) for key, val in self.venues.items()]} response = self.client.post(change_url, data, follow=True) self.assertContains(response, "The following objects will be merged") @@ -71,23 +88,25 @@ class TestAdminMergeObjects(TestCase): self.assertContains(response, venue.name) def test_merge_no_master(self): - change_url = reverse('admin:RIGS_venue_changelist') - data = {'action': 'merge', - '_selected_action': [str(val.pk) for key, val in self.venues.items()], - 'post': 'yes', - } + change_url = reverse("admin:RIGS_venue_changelist") + data = { + "action": "merge", + "_selected_action": [str(val.pk) for key, val in self.venues.items()], + "post": "yes", + } response = self.client.post(change_url, data, follow=True) self.assertContains(response, "An error occured") def test_venue_merge(self): - change_url = reverse('admin:RIGS_venue_changelist') + change_url = reverse("admin:RIGS_venue_changelist") - data = {'action': 'merge', - '_selected_action': [str(self.venues[1].pk), str(self.venues[2].pk)], - 'post': 'yes', - 'master': self.venues[1].pk - } + data = { + "action": "merge", + "_selected_action": [str(self.venues[1].pk), str(self.venues[2].pk)], + "post": "yes", + "master": self.venues[1].pk, + } response = self.client.post(change_url, data, follow=True) self.assertContains(response, "Objects successfully merged") @@ -110,13 +129,14 @@ class TestAdminMergeObjects(TestCase): self.assertEqual(updatedEvent.venue, self.venues[1]) def test_person_merge(self): - change_url = reverse('admin:RIGS_person_changelist') + change_url = reverse("admin:RIGS_person_changelist") - data = {'action': 'merge', - '_selected_action': [str(self.persons[1].pk), str(self.persons[2].pk)], - 'post': 'yes', - 'master': self.persons[1].pk - } + data = { + "action": "merge", + "_selected_action": [str(self.persons[1].pk), str(self.persons[2].pk)], + "post": "yes", + "master": self.persons[1].pk, + } response = self.client.post(change_url, data, follow=True) self.assertContains(response, "Objects successfully merged") @@ -139,13 +159,14 @@ class TestAdminMergeObjects(TestCase): self.assertEqual(updatedEvent.person, self.persons[1]) def test_organisation_merge(self): - change_url = reverse('admin:RIGS_organisation_changelist') + change_url = reverse("admin:RIGS_organisation_changelist") - data = {'action': 'merge', - '_selected_action': [str(self.organisations[1].pk), str(self.organisations[2].pk)], - 'post': 'yes', - 'master': self.organisations[1].pk - } + data = { + "action": "merge", + "_selected_action": [str(self.organisations[1].pk), str(self.organisations[2].pk)], + "post": "yes", + "master": self.organisations[1].pk, + } response = self.client.post(change_url, data, follow=True) self.assertContains(response, "Objects successfully merged") @@ -171,31 +192,33 @@ class TestAdminMergeObjects(TestCase): class TestInvoiceDelete(TestCase): @classmethod def setUpTestData(cls): - cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, - is_active=True, is_staff=True) - cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + cls.profile = models.Profile.objects.create( + username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True + ) + cls.vatrate = models.VatRate.objects.create(start_at="2014-03-05", rate=0.20, comment="test1") cls.events = { 1: models.Event.objects.create(name="TE E1", start_date=date.today()), - 2: models.Event.objects.create(name="TE E2", start_date=date.today()) + 2: models.Event.objects.create(name="TE E2", start_date=date.today()), } cls.invoices = { 1: models.Invoice.objects.create(event=cls.events[1]), - 2: models.Invoice.objects.create(event=cls.events[2]) + 2: models.Invoice.objects.create(event=cls.events[2]), } cls.payments = { - 1: models.Payment.objects.create(invoice=cls.invoices[1], date=date.today(), amount=12.34, - method=models.Payment.CASH) + 1: models.Payment.objects.create( + invoice=cls.invoices[1], date=date.today(), amount=12.34, method=models.Payment.CASH + ) } def setUp(self): - self.profile.set_password('testuser') + self.profile.set_password("testuser") self.profile.save() - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + self.assertTrue(self.client.login(username=self.profile.username, password="testuser")) def test_invoice_delete_allowed(self): - request_url = reverse('invoice_delete', kwargs={'pk': self.invoices[2].pk}) + request_url = reverse("invoice_delete", kwargs={"pk": self.invoices[2].pk}) response = self.client.get(request_url, follow=True) self.assertContains(response, "Are you sure") @@ -210,7 +233,7 @@ class TestInvoiceDelete(TestCase): self.assertRaises(ObjectDoesNotExist, models.Invoice.objects.get, pk=self.invoices[2].pk) def test_invoice_delete_not_allowed(self): - request_url = reverse('invoice_delete', kwargs={'pk': self.invoices[1].pk}) + request_url = reverse("invoice_delete", kwargs={"pk": self.invoices[1].pk}) response = self.client.get(request_url, follow=True) self.assertContains(response, "To delete an invoice, delete the payments first.") @@ -228,11 +251,15 @@ class TestInvoiceDelete(TestCase): class TestPrintPaperwork(TestCase): @classmethod def setUpTestData(cls): - cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, - is_active=True, is_staff=True) + cls.profile = models.Profile.objects.create( + username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True + ) cls.events = { - 1: models.Event.objects.create(name="TE E1", start_date=date.today(), - description="This is an event description\nthat for a very specific reason spans two lines."), + 1: models.Event.objects.create( + name="TE E1", + start_date=date.today(), + description="This is an event description\nthat for a very specific reason spans two lines.", + ), } cls.invoices = { @@ -240,25 +267,25 @@ class TestPrintPaperwork(TestCase): } def setUp(self): - self.profile.set_password('testuser') + self.profile.set_password("testuser") self.profile.save() - self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + self.assertTrue(self.client.login(username=self.profile.username, password="testuser")) def test_print_paperwork_success(self): - request_url = reverse('event_print', kwargs={'pk': self.events[1].pk}) + request_url = reverse("event_print", kwargs={"pk": self.events[1].pk}) response = self.client.get(request_url, follow=True) self.assertEqual(response.status_code, 200) def test_print_invoice_success(self): - request_url = reverse('invoice_print', kwargs={'pk': self.invoices[1].pk}) + request_url = reverse("invoice_print", kwargs={"pk": self.invoices[1].pk}) response = self.client.get(request_url, follow=True) self.assertEqual(response.status_code, 200) def test_login_redirect(client, django_user_model): - request_url = reverse('event_embed', kwargs={'pk': 1}) + request_url = reverse("event_embed", kwargs={"pk": 1}) expected_url = f"{reverse('login_embed')}?next={request_url}" # Request the page and check it redirects @@ -274,14 +301,14 @@ def test_login_redirect(client, django_user_model): def test_login_cookie_warning(client): - login_url = reverse('login_embed') + login_url = reverse("login_embed") response = client.post(login_url, follow=True) assertContains(response, "Cookies do not seem to be enabled") def test_xframe_headers(admin_client, basic_event): - event_url = reverse('event_embed', kwargs={'pk': basic_event.pk}) - login_url = reverse('login_embed') + event_url = reverse("event_embed", kwargs={"pk": basic_event.pk}) + login_url = reverse("login_embed") response = admin_client.get(event_url, follow=True) with pytest.raises(KeyError): @@ -293,12 +320,12 @@ def test_xframe_headers(admin_client, basic_event): def test_oembed(client, basic_event): - event_url = reverse('event_detail', kwargs={'pk': basic_event.pk}) - event_embed_url = reverse('event_embed', kwargs={'pk': basic_event.pk}) - oembed_url = reverse('event_oembed', kwargs={'pk': basic_event.pk}) + event_url = reverse("event_detail", kwargs={"pk": basic_event.pk}) + event_embed_url = reverse("event_embed", kwargs={"pk": basic_event.pk}) + oembed_url = reverse("event_oembed", kwargs={"pk": basic_event.pk}) - alt_oembed_url = reverse('event_oembed', kwargs={'pk': 999}) - alt_event_embed_url = reverse('event_embed', kwargs={'pk': 999}) + alt_oembed_url = reverse("event_oembed", kwargs={"pk": 999}) + alt_event_embed_url = reverse("event_embed", kwargs={"pk": 999}) assert_oembed(alt_event_embed_url, alt_oembed_url, client, event_embed_url, event_url, oembed_url) @@ -308,8 +335,8 @@ def search(client, url, found, notfound, arguments): query = getattr(found, argument) request_url = "%s?q=%s" % (reverse_lazy(url), query) response = client.get(request_url, follow=True) - assertContains(response, getattr(found, 'name')) - assertNotContains(response, getattr(notfound, 'name')) + assertContains(response, getattr(found, "name")) + assertNotContains(response, getattr(notfound, "name")) def test_search(admin_client): @@ -326,47 +353,55 @@ def test_search(admin_client): 2: models.Venue.objects.create(name="Wrong Venue", address="2 Check Way, TS2"), } events = { - 1: models.Event.objects.create(name="Right Event", start_date=date.today(), venue=venues[1], person=persons[1], - organisation=organisations[1]), - 2: models.Event.objects.create(name="Wrong Event", start_date=date.today(), venue=venues[2], person=persons[2], - organisation=organisations[2]), + 1: models.Event.objects.create( + name="Right Event", + start_date=date.today(), + venue=venues[1], + person=persons[1], + organisation=organisations[1], + ), + 2: models.Event.objects.create( + name="Wrong Event", + start_date=date.today(), + venue=venues[2], + person=persons[2], + organisation=organisations[2], + ), } - search(admin_client, 'event_archive', events[1], events[2], ['name', 'id']) - search(admin_client, 'person_list', persons[1], persons[2], ['name', 'id', 'phone']) - search(admin_client, 'organisation_list', organisations[1], organisations[2], - ['name', 'id', 'email']) - search(admin_client, 'venue_list', venues[1], venues[2], - ['name', 'id', 'address']) + search(admin_client, "event_archive", events[1], events[2], ["name", "id"]) + search(admin_client, "person_list", persons[1], persons[2], ["name", "id", "phone"]) + search(admin_client, "organisation_list", organisations[1], organisations[2], ["name", "id", "email"]) + search(admin_client, "venue_list", venues[1], venues[2], ["name", "id", "address"]) def test_hs_list(admin_client, basic_event): - request_url = reverse('hs_list') + request_url = reverse("hs_list") response = admin_client.get(request_url, follow=True) assertContains(response, basic_event.name) # assertContains(response, events[2].name) - assertContains(response, 'Create') + assertContains(response, "Create") def review(client, profile, obj, request_url): time = timezone.now() - response = client.get(reverse(request_url, kwargs={'pk': obj.pk}), follow=True) + response = client.get(reverse(request_url, kwargs={"pk": obj.pk}), follow=True) obj.refresh_from_db() - assertContains(response, 'Reviewed by') + assertContains(response, "Reviewed by") assertContains(response, profile.name) assert_times_almost_equal(time, obj.reviewed_at) def test_ra_review(admin_client, admin_user, ra): - review(admin_client, admin_user, ra, 'ra_review') + review(admin_client, admin_user, ra, "ra_review") def test_checklist_review(admin_client, admin_user, checklist): - review(admin_client, admin_user, checklist, 'ec_review') + review(admin_client, admin_user, checklist, "ec_review") def test_ra_redirect(admin_client, admin_user, ra): - request_url = reverse('event_ra', kwargs={'pk': ra.event.pk}) - expected_url = reverse('ra_edit', kwargs={'pk': ra.pk}) + request_url = reverse("event_ra", kwargs={"pk": ra.event.pk}) + expected_url = reverse("ra_edit", kwargs={"pk": ra.pk}) response = admin_client.get(request_url, follow=True) assertRedirects(response, expected_url, status_code=302, target_status_code=200) @@ -380,19 +415,19 @@ class TestMarkdownTemplateTags(TestCase): self.assertIsInstance(html, SafeText) def test_img_strip(self): - rml = markdown_filter(self.markdown, 'rml') + rml = markdown_filter(self.markdown, "rml") self.assertNotIn("monospace', rml) def test_blockquote(self): - rml = markdown_filter(self.markdown, 'rml') + rml = markdown_filter(self.markdown, "rml") self.assertIn("
    \nBlock quotes", rml)
     
         def test_lists(self):
    -        rml = markdown_filter(self.markdown, 'rml')
    +        rml = markdown_filter(self.markdown, "rml")
             self.assertIn("
  • second item
  • ", rml) #
      self.assertIn("
    1. that one
    2. ", rml) #
        @@ -400,19 +435,21 @@ class TestMarkdownTemplateTags(TestCase): event = models.Event.objects.create( name="MD Print Test", description=self.markdown, - start_date='2016-01-01', + start_date="2016-01-01", + ) + models.EventItem.objects.create( + event=event, name="TI I1", quantity=1, cost=1.00, order=1, description="* test \n * test \n * test" ) - event_item = models.EventItem.objects.create(event=event, name="TI I1", quantity=1, cost=1.00, order=1, description="* test \n * test \n * test") user = models.Profile.objects.create( - username='RML test', + username="RML test", is_superuser=True, # Don't care about permissions is_active=True, ) - user.set_password('rmltester') + user.set_password("rmltester") user.save() - self.assertTrue(self.client.login(username=user.username, password='rmltester')) - response = self.client.get(reverse('event_print', kwargs={'pk': event.pk})) + self.assertTrue(self.client.login(username=user.username, password="rmltester")) + response = self.client.get(reverse("event_print", kwargs={"pk": event.pk})) self.assertEqual(response.status_code, 200) # By the time we have a PDF it should be larger than the original by some margin # RML hard fails if something doesn't work @@ -425,3 +462,24 @@ class TestMarkdownTemplateTags(TestCase): def test_linebreaks(self): html = markdown_filter(self.markdown) self.assertIn("Itemized lists
        \nlook like", html) + + +def test_ra_big_power_without_power_mic_is_a_form_error(ra): + data = model_to_dict(ra) + data.update(big_power=True, power_mic=None, supervisor_consulted=True) + form = forms.EventRiskAssessmentForm(data=data, instance=ra) + assert not form.is_valid() + assert form.errors["power_mic"].as_data()[0].code == "power_mic_required" + + +def test_checkin_person_picker_only_for_event_mic(client, admin_user, basic_event, django_user_model): + other = django_user_model.objects.create_user(username="other", password="other", is_approved=True) + basic_event.mic = admin_user + basic_event.save() + url = reverse("event_checkin", kwargs={"pk": basic_event.pk}) + + client.force_login(admin_user) + assertContains(client.get(url), "selectpicker") + + client.force_login(other) + assertNotContains(client.get(url), "selectpicker") diff --git a/RIGS/urls.py b/RIGS/urls.py index 6c5682ca..260d42a9 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -3,165 +3,267 @@ from django.urls import path, re_path from django.views.decorators.clickjacking import xframe_options_exempt from django.views.generic import RedirectView -from PyRIGS.decorators import (api_key_required, has_oembed, - permission_required_with_403) +from PyRIGS.decorators import api_key_required, has_oembed, permission_required_with_403 from . import views urlpatterns = [ # People - path('people/', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()), - name='person_list'), - path('people/add/', permission_required_with_403('RIGS.add_person')(views.PersonCreate.as_view()), - name='person_create'), - path('people//', permission_required_with_403('RIGS.view_person')(views.PersonDetail.as_view()), - name='person_detail'), - path('people//edit/', permission_required_with_403('RIGS.change_person')(views.PersonUpdate.as_view()), - name='person_update'), - + path("people/", permission_required_with_403("RIGS.view_person")(views.PersonList.as_view()), name="person_list"), + path( + "people/add/", + permission_required_with_403("RIGS.add_person")(views.PersonCreate.as_view()), + name="person_create", + ), + path( + "people//", + permission_required_with_403("RIGS.view_person")(views.PersonDetail.as_view()), + name="person_detail", + ), + path( + "people//edit/", + permission_required_with_403("RIGS.change_person")(views.PersonUpdate.as_view()), + name="person_update", + ), # Organisations - path('organisations/', permission_required_with_403('RIGS.view_organisation')(views.OrganisationList.as_view()), - name='organisation_list'), - path('organisations/add/', - permission_required_with_403('RIGS.add_organisation')(views.OrganisationCreate.as_view()), - name='organisation_create'), - path('organisations//', - permission_required_with_403('RIGS.view_organisation')(views.OrganisationDetail.as_view()), - name='organisation_detail'), - path('organisations//edit/', - permission_required_with_403('RIGS.change_organisation')(views.OrganisationUpdate.as_view()), - name='organisation_update'), - + path( + "organisations/", + permission_required_with_403("RIGS.view_organisation")(views.OrganisationList.as_view()), + name="organisation_list", + ), + path( + "organisations/add/", + permission_required_with_403("RIGS.add_organisation")(views.OrganisationCreate.as_view()), + name="organisation_create", + ), + path( + "organisations//", + permission_required_with_403("RIGS.view_organisation")(views.OrganisationDetail.as_view()), + name="organisation_detail", + ), + path( + "organisations//edit/", + permission_required_with_403("RIGS.change_organisation")(views.OrganisationUpdate.as_view()), + name="organisation_update", + ), # Venues - path('venues/', permission_required_with_403('RIGS.view_venue')(views.VenueList.as_view()), - name='venue_list'), - path('venues/add/', permission_required_with_403('RIGS.add_venue')(views.VenueCreate.as_view()), - name='venue_create'), - path('venues//', permission_required_with_403('RIGS.view_venue')(views.VenueDetail.as_view()), - name='venue_detail'), - path('venues//edit/', permission_required_with_403('RIGS.change_venue')(views.VenueUpdate.as_view()), - name='venue_update'), - + path("venues/", permission_required_with_403("RIGS.view_venue")(views.VenueList.as_view()), name="venue_list"), + path( + "venues/add/", permission_required_with_403("RIGS.add_venue")(views.VenueCreate.as_view()), name="venue_create" + ), + path( + "venues//", + permission_required_with_403("RIGS.view_venue")(views.VenueDetail.as_view()), + name="venue_detail", + ), + path( + "venues//edit/", + permission_required_with_403("RIGS.change_venue")(views.VenueUpdate.as_view()), + name="venue_update", + ), # Rigboard - path('rigboard/', login_required(views.RigboardIndex.as_view()), name='rigboard'), - path('rigboard/calendar/', login_required()(views.WebCalendar.as_view()), - name='web_calendar'), - re_path(r'^rigboard/calendar/(?P(month|week|day))/$', - login_required()(views.WebCalendar.as_view()), name='web_calendar'), - re_path(r'^rigboard/calendar/(?P(month|week|day))/(?P(\d{4}-\d{2}-\d{2}))/$', - login_required()(views.WebCalendar.as_view()), name='web_calendar'), - path('rigboard/archive/', RedirectView.as_view(permanent=True, pattern_name='event_archive')), - - - path('event//', has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()), - name='event_detail'), - path('event/create/', permission_required_with_403('RIGS.add_event')(views.EventCreate.as_view()), - name='event_create'), - path('event/archive/', login_required()(views.EventArchive.as_view()), - name='event_archive'), - path('event//embed/', - xframe_options_exempt(login_required(login_url='/user/login/embed/')(views.EventEmbed.as_view())), - name='event_embed'), - path('event//oembed_json/', views.EventOEmbed.as_view(), - name='event_oembed'), - path('event//print/', permission_required_with_403('RIGS.view_event')(views.EventPrint.as_view()), - name='event_print'), - path('event//edit/', permission_required_with_403('RIGS.change_event')(views.EventUpdate.as_view()), - name='event_update'), - path('event//duplicate/', permission_required_with_403('RIGS.add_event')(views.EventDuplicate.as_view()), - name='event_duplicate'), - + path("rigboard/", login_required(views.RigboardIndex.as_view()), name="rigboard"), + path("rigboard/calendar/", login_required()(views.WebCalendar.as_view()), name="web_calendar"), + re_path( + r"^rigboard/calendar/(?P(month|week|day))/$", + login_required()(views.WebCalendar.as_view()), + name="web_calendar", + ), + re_path( + r"^rigboard/calendar/(?P(month|week|day))/(?P(\d{4}-\d{2}-\d{2}))/$", + login_required()(views.WebCalendar.as_view()), + name="web_calendar", + ), + path("rigboard/archive/", RedirectView.as_view(permanent=True, pattern_name="event_archive")), + path("event//", has_oembed(oembed_view="event_oembed")(views.EventDetail.as_view()), name="event_detail"), + path( + "event/create/", + permission_required_with_403("RIGS.add_event")(views.EventCreate.as_view()), + name="event_create", + ), + path("event/archive/", login_required()(views.EventArchive.as_view()), name="event_archive"), + path( + "event//embed/", + xframe_options_exempt(login_required(login_url="/user/login/embed/")(views.EventEmbed.as_view())), + name="event_embed", + ), + path("event//oembed_json/", views.EventOEmbed.as_view(), name="event_oembed"), + path( + "event//print/", + permission_required_with_403("RIGS.view_event")(views.EventPrint.as_view()), + name="event_print", + ), + path( + "event//edit/", + permission_required_with_403("RIGS.change_event")(views.EventUpdate.as_view()), + name="event_update", + ), + path( + "event//duplicate/", + permission_required_with_403("RIGS.add_event")(views.EventDuplicate.as_view()), + name="event_duplicate", + ), # Event H&S - path('event/hs/', permission_required_with_403('RIGS.view_riskassessment')(views.HSList.as_view()), name='hs_list'), - - path('event//ra/', permission_required_with_403('RIGS.add_riskassessment')(views.EventRiskAssessmentCreate.as_view()), - name='event_ra'), - path('event/ra//', login_required(views.EventRiskAssessmentDetail.as_view()), - name='ra_detail'), - path('event/ra//edit/', permission_required_with_403('RIGS.change_riskassessment')(views.EventRiskAssessmentEdit.as_view()), - name='ra_edit'), - path('event/ra//review/', permission_required_with_403('RIGS.review_riskassessment')(views.MarkReviewed.as_view()), - name='ra_review', kwargs={'model': 'RiskAssessment'}), - path('event/ra//print/', permission_required_with_403('RIGS.view_riskassessment')(views.RAPrint.as_view()), name='ra_print'), - - path('event//checklist/', permission_required_with_403('RIGS.add_eventchecklist')(views.EventChecklistCreate.as_view()), - name='event_ec'), - path('event/checklist//', login_required(views.EventChecklistDetail.as_view()), - name='ec_detail'), - path('event/checklist//edit/', permission_required_with_403('RIGS.change_eventchecklist')(views.EventChecklistEdit.as_view()), - name='ec_edit'), - path('event/checklist//review/', permission_required_with_403('RIGS.review_eventchecklist')(views.MarkReviewed.as_view()), - name='ec_review', kwargs={'model': 'EventChecklist'}), - - path('event//power/', permission_required_with_403('RIGS.add_powertestrecord')(views.PowerTestCreate.as_view()), - name='event_pt'), - path('event/power//', login_required(views.PowerTestDetail.as_view()), - name='pt_detail'), - path('event/power//edit/', permission_required_with_403('RIGS.change_powertestrecord')(views.PowerTestEdit.as_view()), - name='pt_edit'), - path('event/power//review/', permission_required_with_403('RIGS.review_power')(views.MarkReviewed.as_view()), - name='pt_review', kwargs={'model': 'PowerTestRecord'}), - path('event/power//print/', permission_required_with_403('RIGS.view_powertestrecord')(views.PowerPrint.as_view()), name='pt_print'), - - path('event//checkin/', login_required(views.EventCheckIn.as_view()), - name='event_checkin'), - path('event/checkout/', login_required(views.EventCheckOut.as_view()), - name='event_checkout'), - path('event//checkin/edit/', login_required(views.EventCheckInEdit.as_view()), - name='edit_checkin'), - path('event//checkin/add/', login_required(views.EventCheckInOverride.as_view()), - name='event_checkin_override'), - - path('event//thread/', permission_required_with_403('RIGS.change_event')(views.CreateForumThread.as_view()), name='event_thread'), - path('event/webhook/', views.RecieveForumWebhook.as_view(), name='webhook_recieve'), - + path("event/hs/", permission_required_with_403("RIGS.view_riskassessment")(views.HSList.as_view()), name="hs_list"), + path( + "event//ra/", + permission_required_with_403("RIGS.add_riskassessment")(views.EventRiskAssessmentCreate.as_view()), + name="event_ra", + ), + path("event/ra//", login_required(views.EventRiskAssessmentDetail.as_view()), name="ra_detail"), + path( + "event/ra//edit/", + permission_required_with_403("RIGS.change_riskassessment")(views.EventRiskAssessmentEdit.as_view()), + name="ra_edit", + ), + path( + "event/ra//review/", + permission_required_with_403("RIGS.review_riskassessment")(views.MarkReviewed.as_view()), + name="ra_review", + kwargs={"model": "RiskAssessment"}, + ), + path( + "event/ra//print/", + permission_required_with_403("RIGS.view_riskassessment")(views.RAPrint.as_view()), + name="ra_print", + ), + path( + "event//checklist/", + permission_required_with_403("RIGS.add_eventchecklist")(views.EventChecklistCreate.as_view()), + name="event_ec", + ), + path("event/checklist//", login_required(views.EventChecklistDetail.as_view()), name="ec_detail"), + path( + "event/checklist//edit/", + permission_required_with_403("RIGS.change_eventchecklist")(views.EventChecklistEdit.as_view()), + name="ec_edit", + ), + path( + "event/checklist//review/", + permission_required_with_403("RIGS.review_eventchecklist")(views.MarkReviewed.as_view()), + name="ec_review", + kwargs={"model": "EventChecklist"}, + ), + path( + "event//power/", + permission_required_with_403("RIGS.add_powertestrecord")(views.PowerTestCreate.as_view()), + name="event_pt", + ), + path("event/power//", login_required(views.PowerTestDetail.as_view()), name="pt_detail"), + path( + "event/power//edit/", + permission_required_with_403("RIGS.change_powertestrecord")(views.PowerTestEdit.as_view()), + name="pt_edit", + ), + path( + "event/power//review/", + permission_required_with_403("RIGS.review_power")(views.MarkReviewed.as_view()), + name="pt_review", + kwargs={"model": "PowerTestRecord"}, + ), + path( + "event/power//print/", + permission_required_with_403("RIGS.view_powertestrecord")(views.PowerPrint.as_view()), + name="pt_print", + ), + path("event//checkin/", login_required(views.EventCheckIn.as_view()), name="event_checkin"), + path("event/checkout/", login_required(views.EventCheckOut.as_view()), name="event_checkout"), + path("event//checkin/edit/", login_required(views.EventCheckInEdit.as_view()), name="edit_checkin"), + path( + "event//checkin/add/", + login_required(views.EventCheckInOverride.as_view()), + name="event_checkin_override", + ), + path( + "event//thread/", + permission_required_with_403("RIGS.change_event")(views.CreateForumThread.as_view()), + name="event_thread", + ), + path("event/webhook/", views.RecieveForumWebhook.as_view(), name="webhook_recieve"), # Finance - path('invoice/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceDashboard.as_view()), name='invoice_dashboard'), - path('invoice/outstanding', permission_required_with_403('RIGS.view_invoice')(views.InvoiceOutstanding.as_view()), - name='invoice_list'), - path('invoice/archive/', permission_required_with_403('RIGS.view_invoice')(views.InvoiceArchive.as_view()), - name='invoice_archive'), - path('invoice/waiting/', permission_required_with_403('RIGS.add_invoice')(views.InvoiceWaiting.as_view()), - name='invoice_waiting'), - - path('event//invoice/', permission_required_with_403('RIGS.add_invoice')(views.InvoiceEvent.as_view()), - name='invoice_event'), - path('event//invoice/void', permission_required_with_403('RIGS.add_invoice')(views.InvoiceEvent.as_view()), - name='invoice_event_void', kwargs={'void': True}), - - path('invoice//', permission_required_with_403('RIGS.view_invoice')(views.InvoiceDetail.as_view()), - name='invoice_detail'), - path('invoice//print/', permission_required_with_403('RIGS.view_invoice')(views.InvoicePrint.as_view()), - name='invoice_print'), - path('invoice//void/', permission_required_with_403('RIGS.change_invoice')(views.InvoiceVoid.as_view()), - name='invoice_void'), - path('invoice//delete/', - permission_required_with_403('RIGS.change_invoice')(views.InvoiceDelete.as_view()), - name='invoice_delete'), - - path('payment/create/', permission_required_with_403('RIGS.add_payment')(views.PaymentCreate.as_view()), - name='payment_create'), - path('payment//delete/', permission_required_with_403('RIGS.add_payment')(views.PaymentDelete.as_view()), - name='payment_delete'), - + path( + "invoice/", + permission_required_with_403("RIGS.view_invoice")(views.InvoiceDashboard.as_view()), + name="invoice_dashboard", + ), + path( + "invoice/outstanding", + permission_required_with_403("RIGS.view_invoice")(views.InvoiceOutstanding.as_view()), + name="invoice_list", + ), + path( + "invoice/archive/", + permission_required_with_403("RIGS.view_invoice")(views.InvoiceArchive.as_view()), + name="invoice_archive", + ), + path( + "invoice/waiting/", + permission_required_with_403("RIGS.add_invoice")(views.InvoiceWaiting.as_view()), + name="invoice_waiting", + ), + path( + "event//invoice/", + permission_required_with_403("RIGS.add_invoice")(views.InvoiceEvent.as_view()), + name="invoice_event", + ), + path( + "event//invoice/void", + permission_required_with_403("RIGS.add_invoice")(views.InvoiceEvent.as_view()), + name="invoice_event_void", + kwargs={"void": True}, + ), + path( + "invoice//", + permission_required_with_403("RIGS.view_invoice")(views.InvoiceDetail.as_view()), + name="invoice_detail", + ), + path( + "invoice//print/", + permission_required_with_403("RIGS.view_invoice")(views.InvoicePrint.as_view()), + name="invoice_print", + ), + path( + "invoice//void/", + permission_required_with_403("RIGS.change_invoice")(views.InvoiceVoid.as_view()), + name="invoice_void", + ), + path( + "invoice//delete/", + permission_required_with_403("RIGS.change_invoice")(views.InvoiceDelete.as_view()), + name="invoice_delete", + ), + path( + "payment/create/", + permission_required_with_403("RIGS.add_payment")(views.PaymentCreate.as_view()), + name="payment_create", + ), + path( + "payment//delete/", + permission_required_with_403("RIGS.add_payment")(views.PaymentDelete.as_view()), + name="payment_delete", + ), # Client event authorisation - path('event//auth/', - permission_required_with_403('RIGS.change_event')(views.EventAuthorisationRequest.as_view()), - name='event_authorise_request'), - path('event//auth/preview/', - permission_required_with_403('RIGS.change_event')(views.EventAuthoriseRequestEmailPreview.as_view()), - name='event_authorise_preview'), - re_path(r'^event/(?P\d+)/(?P[-:\w]+)/$', views.EventAuthorise.as_view(), - name='event_authorise'), - re_path(r'^event/(?P\d+)/(?P[-:\w]+)/preview/$', views.EventAuthorise.as_view(preview=True), - name='event_authorise_form_preview'), - + path( + "event//auth/", + permission_required_with_403("RIGS.change_event")(views.EventAuthorisationRequest.as_view()), + name="event_authorise_request", + ), + path( + "event//auth/preview/", + permission_required_with_403("RIGS.change_event")(views.EventAuthoriseRequestEmailPreview.as_view()), + name="event_authorise_preview", + ), + re_path(r"^event/(?P\d+)/(?P[-:\w]+)/$", views.EventAuthorise.as_view(), name="event_authorise"), + re_path( + r"^event/(?P\d+)/(?P[-:\w]+)/preview/$", + views.EventAuthorise.as_view(preview=True), + name="event_authorise_form_preview", + ), # ICS Calendar - API key authentication - re_path(r'^ical/(?P\d+)/(?P\w+)/rigs.ics$', api_key_required(views.CalendarICS()), - name="ics_calendar"), - - + re_path( + r"^ical/(?P\d+)/(?P\w+)/rigs.ics$", api_key_required(views.CalendarICS()), name="ics_calendar" + ), # Legacy URLs - path('rig/show//', RedirectView.as_view(permanent=True, pattern_name='event_detail')), - path('bookings/', RedirectView.as_view(permanent=True, pattern_name='rigboard')), - path('bookings/past/', RedirectView.as_view(permanent=True, pattern_name='event_archive')), + path("rig/show//", RedirectView.as_view(permanent=True, pattern_name="event_detail")), + path("bookings/", RedirectView.as_view(permanent=True, pattern_name="rigboard")), + path("bookings/past/", RedirectView.as_view(permanent=True, pattern_name="event_archive")), ] diff --git a/RIGS/views/crud.py b/RIGS/views/crud.py index b1aa607e..8ad27207 100644 --- a/RIGS/views/crud.py +++ b/RIGS/views/crud.py @@ -7,11 +7,11 @@ class PersonList(GenericListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = "People" - context['create'] = 'person_create' - context['edit'] = 'person_update' - context['can_edit'] = self.request.user.has_perm('RIGS.change_person') - context['detail'] = 'person_detail' + context["page_title"] = "People" + context["create"] = "person_create" + context["edit"] = "person_update" + context["can_edit"] = self.request.user.has_perm("RIGS.change_person") + context["detail"] = "person_detail" return context @@ -20,29 +20,29 @@ class PersonDetail(GenericDetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['history_link'] = 'person_history' - context['detail_link'] = 'person_detail' - context['update_link'] = 'person_update' - context['associated'] = 'partials/associated_organisations.html' - context['associated2'] = 'partials/associated_events.html' - context['can_edit'] = self.request.user.has_perm('RIGS.change_person') + context["history_link"] = "person_history" + context["detail_link"] = "person_detail" + context["update_link"] = "person_update" + context["associated"] = "partials/associated_organisations.html" + context["associated2"] = "partials/associated_events.html" + context["can_edit"] = self.request.user.has_perm("RIGS.change_person") return context class PersonCreate(GenericCreateView, ModalURLMixin): model = models.Person - fields = ['name', 'phone', 'email', 'address', 'notes'] + fields = ["name", "phone", "email", "address", "notes"] def get_success_url(self): - return self.get_close_url('person_update', 'person_detail') + return self.get_close_url("person_update", "person_detail") class PersonUpdate(GenericUpdateView, ModalURLMixin): model = models.Person - fields = ['name', 'phone', 'email', 'address', 'notes'] + fields = ["name", "phone", "email", "address", "notes"] def get_success_url(self): - return self.get_close_url('person_update', 'person_detail') + return self.get_close_url("person_update", "person_detail") class OrganisationList(GenericListView): @@ -50,11 +50,11 @@ class OrganisationList(GenericListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['create'] = 'organisation_create' - context['edit'] = 'organisation_update' - context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation') - context['detail'] = 'organisation_detail' - context['union_account'] = True + context["create"] = "organisation_create" + context["edit"] = "organisation_update" + context["can_edit"] = self.request.user.has_perm("RIGS.change_organisation") + context["detail"] = "organisation_detail" + context["union_account"] = True return context @@ -63,29 +63,29 @@ class OrganisationDetail(GenericDetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['history_link'] = 'organisation_history' - context['detail_link'] = 'organisation_detail' - context['update_link'] = 'organisation_update' - context['associated'] = 'partials/associated_people.html' - context['associated2'] = 'partials/associated_events.html' - context['can_edit'] = self.request.user.has_perm('RIGS.change_organisation') + context["history_link"] = "organisation_history" + context["detail_link"] = "organisation_detail" + context["update_link"] = "organisation_update" + context["associated"] = "partials/associated_people.html" + context["associated2"] = "partials/associated_events.html" + context["can_edit"] = self.request.user.has_perm("RIGS.change_organisation") return context class OrganisationCreate(GenericCreateView, ModalURLMixin): model = models.Organisation - fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account'] + fields = ["name", "phone", "email", "address", "notes", "union_account"] def get_success_url(self): - return self.get_close_url('organisation_update', 'organisation_detail') + return self.get_close_url("organisation_update", "organisation_detail") class OrganisationUpdate(GenericUpdateView, ModalURLMixin): model = models.Organisation - fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account'] + fields = ["name", "phone", "email", "address", "notes", "union_account"] def get_success_url(self): - return self.get_close_url('organisation_update', 'organisation_detail') + return self.get_close_url("organisation_update", "organisation_detail") class VenueList(GenericListView): @@ -93,10 +93,10 @@ class VenueList(GenericListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['create'] = 'venue_create' - context['edit'] = 'venue_update' - context['can_edit'] = self.request.user.has_perm('RIGS.change_venue') - context['detail'] = 'venue_detail' + context["create"] = "venue_create" + context["edit"] = "venue_update" + context["can_edit"] = self.request.user.has_perm("RIGS.change_venue") + context["detail"] = "venue_detail" return context @@ -105,25 +105,25 @@ class VenueDetail(GenericDetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['history_link'] = 'venue_history' - context['detail_link'] = 'venue_detail' - context['update_link'] = 'venue_update' - context['associated2'] = 'partials/associated_events.html' - context['can_edit'] = self.request.user.has_perm('RIGS.change_venue') + context["history_link"] = "venue_history" + context["detail_link"] = "venue_detail" + context["update_link"] = "venue_update" + context["associated2"] = "partials/associated_events.html" + context["can_edit"] = self.request.user.has_perm("RIGS.change_venue") return context class VenueCreate(GenericCreateView, ModalURLMixin): model = models.Venue - fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available'] + fields = ["name", "phone", "email", "address", "notes", "three_phase_available"] def get_success_url(self): - return self.get_close_url('venue_update', 'venue_detail') + return self.get_close_url("venue_update", "venue_detail") class VenueUpdate(GenericUpdateView, ModalURLMixin): model = models.Venue - fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available'] + fields = ["name", "phone", "email", "address", "notes", "three_phase_available"] def get_success_url(self): - return self.get_close_url('venue_update', 'venue_detail') + return self.get_close_url("venue_update", "venue_detail") diff --git a/RIGS/views/finance.py b/RIGS/views/finance.py index 1f181a63..92ebc5cb 100644 --- a/RIGS/views/finance.py +++ b/RIGS/views/finance.py @@ -16,7 +16,7 @@ from z3c.rml import rml2pdf from RIGS import models -forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) +forms.DateField.widget = forms.DateInput(attrs={"type": "date"}) TIME_FILTERS = ["all", "year", "month", "week"] @@ -27,53 +27,73 @@ def days_between(d1, d2): class InvoiceDashboard(generic.TemplateView): - template_name = 'invoice_dashboard.html' + template_name = "invoice_dashboard.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = "Invoice Dashboard" - context['description'] = "Overview of financial status of TEC rigs." + context["page_title"] = "Invoice Dashboard" + context["description"] = "Overview of financial status of TEC rigs." - time_filter = self.request.GET.get('time_filter', 'all') + time_filter = self.request.GET.get("time_filter", "all") if time_filter not in TIME_FILTERS: - time_filter = 'all' + time_filter = "all" - if time_filter == 'all': - context['events'] = models.Event.objects.filter(is_rig=True) - context['invoices'] = models.Invoice.objects.all() - context['payments'] = models.Payment.objects.all() - elif time_filter == 'year': - context['events'] = models.Event.objects.filter(is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=365)) - context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=365)) - context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=365)) - elif time_filter == 'month': - context['events'] = models.Event.objects.filter(is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=30)) - context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=30)) - context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=30)) - elif time_filter == 'week': - context['events'] = models.Event.objects.filter(is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=7)) - context['invoices'] = models.Invoice.objects.filter(invoice_date__gte=datetime.date.today() - datetime.timedelta(days=7)) - context['payments'] = models.Payment.objects.filter(date__gte=datetime.date.today() - datetime.timedelta(days=7)) + if time_filter == "all": + context["events"] = models.Event.objects.filter(is_rig=True) + context["invoices"] = models.Invoice.objects.all() + context["payments"] = models.Payment.objects.all() + elif time_filter == "year": + context["events"] = models.Event.objects.filter( + is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=365) + ) + context["invoices"] = models.Invoice.objects.filter( + invoice_date__gte=datetime.date.today() - datetime.timedelta(days=365) + ) + context["payments"] = models.Payment.objects.filter( + date__gte=datetime.date.today() - datetime.timedelta(days=365) + ) + elif time_filter == "month": + context["events"] = models.Event.objects.filter( + is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=30) + ) + context["invoices"] = models.Invoice.objects.filter( + invoice_date__gte=datetime.date.today() - datetime.timedelta(days=30) + ) + context["payments"] = models.Payment.objects.filter( + date__gte=datetime.date.today() - datetime.timedelta(days=30) + ) + elif time_filter == "week": + context["events"] = models.Event.objects.filter( + is_rig=True, start_date__gte=datetime.date.today() - datetime.timedelta(days=7) + ) + context["invoices"] = models.Invoice.objects.filter( + invoice_date__gte=datetime.date.today() - datetime.timedelta(days=7) + ) + context["payments"] = models.Payment.objects.filter( + date__gte=datetime.date.today() - datetime.timedelta(days=7) + ) context["time_filter"] = time_filter - context['total_outstanding'] = sum([i.balance for i in models.Invoice.objects.outstanding_invoices()]) - context['total_waiting'] = sum([i.sum_total for i in models.Event.objects.waiting_invoices()]) - context['total_events'] = len(context['events']) - context['total_invoices'] = len(context['invoices']) - context['total_payments'] = len(context['payments']) + context["total_outstanding"] = sum([i.balance for i in models.Invoice.objects.outstanding_invoices()]) + context["total_waiting"] = sum([i.sum_total for i in models.Event.objects.waiting_invoices()]) + context["total_events"] = len(context["events"]) + context["total_invoices"] = len(context["invoices"]) + context["total_payments"] = len(context["payments"]) payment_methods = dict(models.Payment.METHODS) - context['payment_methods'] = context["payments"].values('method').annotate(total=Sum('amount')).order_by('method') + context["payment_methods"] = ( + context["payments"].values("method").annotate(total=Sum("amount")).order_by("method") + ) - for method in context['payment_methods']: - method['method'] = payment_methods.get(method['method'], f"Unknown method ({method['method']})") + for method in context["payment_methods"]: + method["method"] = payment_methods.get(method["method"], f"Unknown method ({method['method']})") - context["total_income"] = sum([i['total'] for i in context['payment_methods']]) + context["total_income"] = sum([i["total"] for i in context["payment_methods"]]) - payments = context['payments'] + payments = context["payments"] mean_duration = 0 for payment in payments: @@ -82,23 +102,25 @@ class InvoiceDashboard(generic.TemplateView): if len(payments) > 0: mean_duration /= len(payments) - context['mean_invoice_to_payment'] = mean_duration + context["mean_invoice_to_payment"] = mean_duration return context class InvoiceOutstanding(generic.ListView): model = models.Invoice - template_name = 'invoice_list.html' + template_name = "invoice_list.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) total = 0 - for i in context['object_list']: + for i in context["object_list"]: total += i.balance - event_count = len(list(context['object_list'])) - context['page_title'] = f"Outstanding Invoices ({event_count} Events, £{total:.2f})" - context['description'] = "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger" + event_count = len(list(context["object_list"])) + context["page_title"] = f"Outstanding Invoices ({event_count} Events, £{total:.2f})" + context["description"] = ( + "Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger" + ) return context def get_queryset(self): @@ -107,18 +129,18 @@ class InvoiceOutstanding(generic.ListView): class InvoiceDetail(generic.DetailView): model = models.Invoice - template_name = 'invoice_detail.html' + template_name = "invoice_detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) invoice_date = self.object.invoice_date.strftime("%d/%m/%Y") - context['page_title'] = f"Invoice {self.object.display_id} ({invoice_date})" + context["page_title"] = f"Invoice {self.object.display_id} ({invoice_date})" if self.object.void: - context['page_title'] += "VOID" + context["page_title"] += "VOID" elif self.object.is_closed: - context['page_title'] += "PAID" + context["page_title"] += "PAID" else: - context['page_title'] += "OUTSTANDING" + context["page_title"] += "OUTSTANDING" return context @@ -126,17 +148,12 @@ class InvoicePrint(generic.View): def get(self, request, pk): invoice = get_object_or_404(models.Invoice, pk=pk) object = invoice.event - template = get_template('event_print.xml') + template = get_template("event_print.xml") - name = re.sub(r'[^a-zA-Z0-9 \n\.]', '', object.name) + name = re.sub(r"[^a-zA-Z0-9 \n\.]", "", object.name) filename = f"Invoice {invoice.display_id} for {object.display_id} {name}.pdf" - context = { - 'object': object, - 'invoice': invoice, - 'current_user': request.user, - 'filename': filename - } + context = {"object": object, "invoice": invoice, "current_user": request.user, "filename": filename} rml = template.render(context) @@ -144,65 +161,65 @@ class InvoicePrint(generic.View): pdfData = buffer.read() - response = HttpResponse(content_type='application/pdf') - response['Content-Disposition'] = f'filename="{filename}"' + response = HttpResponse(content_type="application/pdf") + response["Content-Disposition"] = f'filename="{filename}"' response.write(pdfData) return response class InvoiceVoid(generic.View): def get(self, *args, **kwargs): - pk = kwargs.get('pk') + pk = kwargs.get("pk") object = get_object_or_404(models.Invoice, pk=pk) object.void = not object.void object.save() if object.void: - return HttpResponseRedirect(reverse('invoice_list')) - return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': object.pk})) + return HttpResponseRedirect(reverse("invoice_list")) + return HttpResponseRedirect(reverse("invoice_detail", kwargs={"pk": object.pk})) class InvoiceDelete(generic.DeleteView): model = models.Invoice - template_name = 'invoice_confirm_delete.html' + template_name = "invoice_confirm_delete.html" def get(self, request, pk): obj = self.get_object() if obj.payment_set.all().count() > 0: - messages.info(self.request, 'To delete an invoice, delete the payments first.') - return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk})) + messages.info(self.request, "To delete an invoice, delete the payments first.") + return HttpResponseRedirect(reverse("invoice_detail", kwargs={"pk": obj.pk})) return super(InvoiceDelete, self).get(pk) def post(self, request, pk): obj = self.get_object() if obj.payment_set.all().count() > 0: - messages.info(self.request, 'To delete an invoice, delete the payments first.') - return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': obj.pk})) + messages.info(self.request, "To delete an invoice, delete the payments first.") + return HttpResponseRedirect(reverse("invoice_detail", kwargs={"pk": obj.pk})) return super(InvoiceDelete, self).post(pk) def get_success_url(self): - return self.request.POST.get('next') + return self.request.POST.get("next") class InvoiceArchive(generic.ListView): model = models.Invoice - template_name = 'invoice_list_archive.html' + template_name = "invoice_list_archive.html" paginate_by = 25 def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = "Invoice Archive" - context['description'] = "This page displays all invoices: outstanding, paid, and void" + context["page_title"] = "Invoice Archive" + context["description"] = "This page displays all invoices: outstanding, paid, and void" return context def get_queryset(self): - return self.model.objects.search(self.request.GET.get('q')).order_by('-invoice_date') + return self.model.objects.search(self.request.GET.get("q")).order_by("-invoice_date") class InvoiceWaiting(generic.ListView): model = models.Event paginate_by = 25 - template_name = 'invoice_list_waiting.html' + template_name = "invoice_list_waiting.html" def get_context_data(self, **kwargs): context = super(InvoiceWaiting, self).get_context_data(**kwargs) @@ -210,7 +227,7 @@ class InvoiceWaiting(generic.ListView): objects = self.get_queryset() for obj in objects: total += obj.sum_total - context['page_title'] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})" + context["page_title"] = f"Events for Invoice ({len(objects)} Events, £{total:.2f})" return context def get_queryset(self): @@ -222,51 +239,51 @@ class InvoiceEvent(generic.View): @reversion.create_revision() def get(self, *args, **kwargs): reversion.set_user(self.request.user) - epk = kwargs.get('pk') + epk = kwargs.get("pk") event = models.Event.objects.get(pk=epk) invoice, created = models.Invoice.objects.get_or_create(event=event) if created: invoice.invoice_date = datetime.date.today() - messages.success(self.request, 'Invoice created successfully') + messages.success(self.request, "Invoice created successfully") - if kwargs.get('void'): + if kwargs.get("void"): invoice.void = not invoice.void invoice.save() - messages.warning(self.request, 'Invoice voided') + messages.warning(self.request, "Invoice voided") - return HttpResponseRedirect(reverse('invoice_detail', kwargs={'pk': invoice.pk})) + return HttpResponseRedirect(reverse("invoice_detail", kwargs={"pk": invoice.pk})) class PaymentCreate(generic.CreateView): model = models.Payment - fields = ['invoice', 'date', 'amount', 'method'] - template_name = 'payment_form.html' + fields = ["invoice", "date", "amount", "method"] + template_name = "payment_form.html" def get_initial(self): initial = super().get_initial() - invoicepk = self.request.GET.get('invoice', self.request.POST.get('invoice', None)) + invoicepk = self.request.GET.get("invoice", self.request.POST.get("invoice", None)) if invoicepk is None: raise Http404() invoice = get_object_or_404(models.Invoice, pk=invoicepk) - initial.update({'invoice': invoice}) + initial.update({"invoice": invoice}) return initial @transaction.atomic() @reversion.create_revision() def form_valid(self, form, *args, **kwargs): - reversion.add_to_revision(form.cleaned_data['invoice']) + reversion.add_to_revision(form.cleaned_data["invoice"]) reversion.set_comment("Payment added") return super().form_valid(form, *args, **kwargs) def get_success_url(self): messages.info(self.request, "location.reload()") - return reverse('closemodal') + return reverse("closemodal") class PaymentDelete(generic.DeleteView): model = models.Payment - template_name = 'payment_confirm_delete.html' + template_name = "payment_confirm_delete.html" @transaction.atomic() @reversion.create_revision() @@ -276,4 +293,4 @@ class PaymentDelete(generic.DeleteView): return super().delete(*args, **kwargs) def get_success_url(self): - return self.request.POST.get('next') + return self.request.POST.get("next") diff --git a/RIGS/views/hs.py b/RIGS/views/hs.py index 931bc708..0a772314 100644 --- a/RIGS/views/hs.py +++ b/RIGS/views/hs.py @@ -15,48 +15,48 @@ from django.shortcuts import redirect class HSCreateView(generic.CreateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - event = models.Event.objects.get(pk=self.kwargs.get('pk')) - context['event'] = event - context['page_title'] = f'Create {self.model.__name__} for Event {event.display_id}' - get_related(context['form'], context) + event = models.Event.objects.get(pk=self.kwargs.get("pk")) + context["event"] = event + context["page_title"] = f"Create {self.model.__name__} for Event {event.display_id}" + get_related(context["form"], context) return context class MarkReviewed(generic.RedirectView): def get_redirect_url(self, *args, **kwargs): - obj = apps.get_model('RIGS', kwargs.get('model')).objects.get(pk=kwargs.get('pk')) + obj = apps.get_model("RIGS", kwargs.get("model")).objects.get(pk=kwargs.get("pk")) with reversion.create_revision(): reversion.set_user(self.request.user) obj.reviewed_by = self.request.user obj.reviewed_at = timezone.now() obj.save() - return self.request.META.get('HTTP_REFERER', reverse('hs_list')) + return self.request.META.get("HTTP_REFERER", reverse("hs_list")) class EventRiskAssessmentCreate(HSCreateView): model = models.RiskAssessment - template_name = 'hs/risk_assessment_form.html' + template_name = "hs/risk_assessment_form.html" form_class = forms.EventRiskAssessmentForm def get(self, *args, **kwargs): - epk = kwargs.get('pk') + epk = kwargs.get("pk") event = models.Event.objects.get(pk=epk) # Check if RA exists ra = models.RiskAssessment.objects.filter(event=event).first() if ra is not None: - return HttpResponseRedirect(reverse('ra_edit', kwargs={'pk': ra.pk})) + return HttpResponseRedirect(reverse("ra_edit", kwargs={"pk": ra.pk})) return super().get(self) def get_success_url(self): - return reverse('ra_detail', kwargs={'pk': self.object.pk}) + return reverse("ra_detail", kwargs={"pk": self.object.pk}) class EventRiskAssessmentEdit(generic.UpdateView): model = models.RiskAssessment - template_name = 'hs/risk_assessment_form.html' + template_name = "hs/risk_assessment_form.html" form_class = forms.EventRiskAssessmentForm def get_success_url(self): @@ -64,42 +64,46 @@ class EventRiskAssessmentEdit(generic.UpdateView): ra.reviewed_by = None ra.reviewed_at = None ra.save() - return reverse('ra_detail', kwargs={'pk': self.object.pk}) + return reverse("ra_detail", kwargs={"pk": self.object.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - rpk = self.kwargs.get('pk') + rpk = self.kwargs.get("pk") ra = models.RiskAssessment.objects.get(pk=rpk) - context['event'] = ra.event - context['edit'] = True - context['page_title'] = f'Edit Risk Assessment for Event {ra.event.display_id}' - get_related(context['form'], context) + context["event"] = ra.event + context["edit"] = True + context["page_title"] = f"Edit Risk Assessment for Event {ra.event.display_id}" + get_related(context["form"], context) return context class EventRiskAssessmentDetail(generic.DetailView): model = models.RiskAssessment - template_name = 'hs/risk_assessment_detail.html' + template_name = "hs/risk_assessment_detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"Risk Assessment for Event {self.object.event.display_id} {self.object.event.name}" + context["page_title"] = ( + f"Risk Assessment for Event {self.object.event.display_id} {self.object.event.name}" + ) return context class EventChecklistDetail(generic.DetailView): model = models.EventChecklist - template_name = 'hs/event_checklist_detail.html' + template_name = "hs/event_checklist_detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"Event Checklist for Event {self.object.event.display_id} {self.object.event.name}" + context["page_title"] = ( + f"Event Checklist for Event {self.object.event.display_id} {self.object.event.name}" + ) return context class EventChecklistEdit(generic.UpdateView): model = models.EventChecklist - template_name = 'hs/event_checklist_form.html' + template_name = "hs/event_checklist_form.html" form_class = forms.EventChecklistForm def get_success_url(self): @@ -107,58 +111,63 @@ class EventChecklistEdit(generic.UpdateView): ec.reviewed_by = None ec.reviewed_at = None ec.save() - return reverse('ec_detail', kwargs={'pk': self.object.pk}) + return reverse("ec_detail", kwargs={"pk": self.object.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - pk = self.kwargs.get('pk') + pk = self.kwargs.get("pk") ec = models.EventChecklist.objects.get(pk=pk) - context['event'] = ec.event - context['edit'] = True - context['page_title'] = f'Edit Event Checklist for Event {ec.event.display_id}' - get_related(context['form'], context) + context["event"] = ec.event + context["edit"] = True + context["page_title"] = f"Edit Event Checklist for Event {ec.event.display_id}" + get_related(context["form"], context) return context class EventChecklistCreate(HSCreateView): model = models.EventChecklist - template_name = 'hs/event_checklist_form.html' + template_name = "hs/event_checklist_form.html" form_class = forms.EventChecklistForm # From both business logic and programming POVs, RAs must exist before ECs! def get(self, *args, **kwargs): - epk = kwargs.get('pk') + epk = kwargs.get("pk") event = models.Event.objects.get(pk=epk) # Check if RA exists ra = models.RiskAssessment.objects.filter(event=event).first() if ra is None: - messages.error(self.request, f'A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.') - return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk})) + messages.error( + self.request, + f"A Risk Assessment must exist prior to creating any Event Checklists for {event}! Please create one now.", + ) + return HttpResponseRedirect(reverse("event_ra", kwargs={"pk": epk})) return super().get(self) def get_success_url(self): - return reverse('ec_detail', kwargs={'pk': self.object.pk}) + return reverse("ec_detail", kwargs={"pk": self.object.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - if context['event'].venue: - context['venue'] = context['event'].venue + if context["event"].venue: + context["venue"] = context["event"].venue return context class PowerTestDetail(generic.DetailView): model = models.PowerTestRecord - template_name = 'hs/power_detail.html' + template_name = "hs/power_detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"Power Test Record for Event {self.object.event.display_id} {self.object.event.name}" + context["page_title"] = ( + f"Power Test Record for Event {self.object.event.display_id} {self.object.event.name}" + ) return context class PowerTestEdit(generic.UpdateView): model = models.PowerTestRecord - template_name = 'hs/power_form.html' + template_name = "hs/power_form.html" form_class = forms.PowerTestRecordForm def get_success_url(self): @@ -166,130 +175,140 @@ class PowerTestEdit(generic.UpdateView): ec.reviewed_by = None ec.reviewed_at = None ec.save() - return reverse('pt_detail', kwargs={'pk': self.object.pk}) + return reverse("pt_detail", kwargs={"pk": self.object.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - pk = self.kwargs.get('pk') + pk = self.kwargs.get("pk") ec = models.PowerTestRecord.objects.get(pk=pk) - context['event'] = ec.event - context['edit'] = True - context['page_title'] = f'Edit Power Test Record for Event {ec.event.display_id}' - get_related(context['form'], context) + context["event"] = ec.event + context["edit"] = True + context["page_title"] = f"Edit Power Test Record for Event {ec.event.display_id}" + get_related(context["form"], context) return context class PowerTestCreate(HSCreateView): model = models.PowerTestRecord - template_name = 'hs/power_form.html' + template_name = "hs/power_form.html" form_class = forms.PowerTestRecordForm def get(self, *args, **kwargs): - epk = kwargs.get('pk') + epk = kwargs.get("pk") event = models.Event.objects.get(pk=epk) # Check if RA exists ra = models.RiskAssessment.objects.filter(event=event).first() if ra is None: - messages.error(self.request, f'A Risk Assessment must exist prior to creating any Power Test Records for {event}! Please create one now.') - return HttpResponseRedirect(reverse('event_ra', kwargs={'pk': epk})) + messages.error( + self.request, + f"A Risk Assessment must exist prior to creating any Power Test Records for {event}! Please create one now.", + ) + return HttpResponseRedirect(reverse("event_ra", kwargs={"pk": epk})) return super().get(self) def get_success_url(self): - return reverse('pt_detail', kwargs={'pk': self.object.pk}) + return reverse("pt_detail", kwargs={"pk": self.object.pk}) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - if context['event'].venue: - context['venue'] = context['event'].venue - if context['event'].riskassessment.power_mic: - context['power_mic'] = context['event'].riskassessment.power_mic + if context["event"].venue: + context["venue"] = context["event"].venue + if context["event"].riskassessment.power_mic: + context["power_mic"] = context["event"].riskassessment.power_mic return context class HSList(generic.ListView): paginate_by = 20 model = models.Event - template_name = 'hs/hs_list.html' + template_name = "hs/hs_list.html" def get_queryset(self): - return models.Event.objects.all().exclude(status=models.Event.CANCELLED).exclude(dry_hire=True).order_by('-start_date').select_related('riskassessment').prefetch_related('checklists') + return ( + models.Event.objects.all() + .exclude(status=models.Event.CANCELLED) + .exclude(dry_hire=True) + .order_by("-start_date") + .select_related("riskassessment") + .prefetch_related("checklists") + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = 'H&S Overview' + context["page_title"] = "H&S Overview" return context class RAPrint(PrintView): model = models.RiskAssessment - template_name = 'hs/ra_print.xml' + template_name = "hs/ra_print.xml" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['filename'] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf" + context["filename"] = f"EventSpecificRiskAssessment_for_{context['object'].event.display_id}.pdf" return context class PowerPrint(PrintView): model = models.PowerTestRecord - template_name = 'hs/power_print.xml' + template_name = "hs/power_print.xml" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['filename'] = f"PowerTestRecord_for_{context['object'].event.display_id}.pdf" + context["filename"] = f"PowerTestRecord_for_{context['object'].event.display_id}.pdf" return context class EventCheckIn(generic.CreateView, ModalURLMixin): model = models.EventCheckIn - template_name = 'hs/eventcheckin_form.html' + template_name = "hs/eventcheckin_form.html" form_class = forms.EventCheckInForm def get_success_url(self): - return self.get_close_url('event_detail', 'event_detail') # Well, that's one way of doing that...! + return self.get_close_url("event_detail", "event_detail") # Well, that's one way of doing that...! def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk')) - context['page_title'] = f'Check In to Event {context["event"].display_id}' + context["event"] = models.Event.objects.get(pk=self.kwargs.get("pk")) + context["page_title"] = f"Check In to Event {context['event'].display_id}" # get_related(context['form'], context) return context class EventCheckInOverride(generic.CreateView): model = models.EventCheckIn - template_name = 'hs/eventcheckin_form.html' + template_name = "hs/eventcheckin_form.html" form_class = forms.EditCheckInForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['event'] = models.Event.objects.get(pk=self.kwargs.get('pk')) - context['page_title'] = f'Manually add Check In to Event {context["event"].display_id}' - context['manual'] = True + context["event"] = models.Event.objects.get(pk=self.kwargs.get("pk")) + context["page_title"] = f"Manually add Check In to Event {context['event'].display_id}" + context["manual"] = True return context class EventCheckInEdit(generic.UpdateView, ModalURLMixin): model = models.EventCheckIn - template_name = 'hs/eventcheckin_form.html' + template_name = "hs/eventcheckin_form.html" form_class = forms.EditCheckInForm def dispatch(self, request, *args, **kwargs): obj = self.get_object() if not obj.person == self.request.user and not obj.event.mic == self.request.user: - return redirect(self.request.META.get('HTTP_REFERER', '/')) + return redirect(self.request.META.get("HTTP_REFERER", "/")) return super().dispatch(request) def get_success_url(self): - return self.get_close_url('event_detail', 'event_detail') # Well, that's one way of doing that...! + return self.get_close_url("event_detail", "event_detail") # Well, that's one way of doing that...! def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['event'] = self.object.event - context['page_title'] = f'Edit Check In for Event {context["event"].display_id}' - context['edit'] = True + context["event"] = self.object.event + context["page_title"] = f"Edit Check In for Event {context['event'].display_id}" + context["edit"] = True # get_related(context['form'], context) return context @@ -300,4 +319,4 @@ class EventCheckOut(generic.RedirectView): if checkin: checkin.end_time = timezone.now() checkin.save() - return self.request.META.get('HTTP_REFERER', '/') + return self.request.META.get("HTTP_REFERER", "/") diff --git a/RIGS/views/ical.py b/RIGS/views/ical.py index aacea3c1..139289b9 100644 --- a/RIGS/views/ical.py +++ b/RIGS/views/ical.py @@ -12,9 +12,10 @@ class CalendarICS(ICalFeed): """ A simple event calender """ + # Metadata which is passed on to clients - product_id = 'RIGS' - title = 'RIGS Calendar' + product_id = "RIGS" + title = "RIGS Calendar" timezone = settings.TIME_ZONE file_name = "rigs.ics" @@ -29,24 +30,34 @@ class CalendarICS(ICalFeed): def get_object(self, request, *args, **kwargs): params = {} - params['dry-hire'] = request.GET.get('dry-hire', 'true') == 'true' - params['non-rig'] = request.GET.get('non-rig', 'true') == 'true' - params['rig'] = request.GET.get('rig', 'true') == 'true' + params["dry-hire"] = request.GET.get("dry-hire", "true") == "true" + params["non-rig"] = request.GET.get("non-rig", "true") == "true" + params["rig"] = request.GET.get("rig", "true") == "true" - params['cancelled'] = request.GET.get('cancelled', 'false') == 'true' - params['provisional'] = request.GET.get('provisional', 'true') == 'true' - params['confirmed'] = request.GET.get('confirmed', 'true') == 'true' - params['only_mic'] = request.GET.get('only_mic', 'false') == 'true' + params["cancelled"] = request.GET.get("cancelled", "false") == "true" + params["provisional"] = request.GET.get("provisional", "true") == "true" + params["confirmed"] = request.GET.get("confirmed", "true") == "true" + params["only_mic"] = request.GET.get("only_mic", "false") == "true" - params['user'] = kwargs['user'] + params["user"] = kwargs["user"] return params def description(self, params): - desc = "Calendar generated by RIGS system. This includes event types: " + ('Rig, ' if params['rig'] else '') + ( - 'Non-rig, ' if params['non-rig'] else '') + ('Dry Hire ' if params['dry-hire'] else '') + '\n' - desc = desc + "Includes events with status: " + ('Cancelled, ' if params['cancelled'] else '') + ( - 'Provisional, ' if params['provisional'] else '') + ('Confirmed/Booked, ' if params['confirmed'] else '') + desc = ( + "Calendar generated by RIGS system. This includes event types: " + + ("Rig, " if params["rig"] else "") + + ("Non-rig, " if params["non-rig"] else "") + + ("Dry Hire " if params["dry-hire"] else "") + + "\n" + ) + desc = ( + desc + + "Includes events with status: " + + ("Cancelled, " if params["cancelled"] else "") + + ("Provisional, " if params["provisional"] else "") + + ("Confirmed/Booked, " if params["confirmed"] else "") + ) return desc @@ -57,50 +68,53 @@ class CalendarICS(ICalFeed): typeFilters = Q(pk=None) # Need something that is false for every entry - if params['dry-hire']: + if params["dry-hire"]: typeFilters = typeFilters | Q(dry_hire=True, is_rig=True) - if params['non-rig']: + if params["non-rig"]: typeFilters = typeFilters | Q(is_rig=False) - if params['rig']: + if params["rig"]: typeFilters = typeFilters | Q(is_rig=True, dry_hire=False) statusFilters = Q(pk=None) # Need something that is false for every entry - if params['cancelled']: + if params["cancelled"]: statusFilters = statusFilters | Q(status=models.Event.CANCELLED) - if params['provisional']: + if params["provisional"]: statusFilters = statusFilters | Q(status=models.Event.PROVISIONAL) - if params['confirmed']: + if params["confirmed"]: statusFilters = statusFilters | Q(status=models.Event.CONFIRMED) | Q(status=models.Event.BOOKED) filter = filter & typeFilters & statusFilters - if params['only_mic']: - filter = filter & Q(mic=params['user']) + if params["only_mic"]: + filter = filter & Q(mic=params["user"]) - return models.Event.objects.filter(filter).order_by('-start_date').select_related('person', 'organisation', - 'venue', 'mic') + return ( + models.Event.objects.filter(filter) + .order_by("-start_date") + .select_related("person", "organisation", "venue", "mic") + ) def item_title(self, item): - title = '' + title = "" # Prefix title with status (if it's a critical status) if item.cancelled: - title += 'CANCELLED: ' + title += "CANCELLED: " if not item.is_rig: - title += 'NON-RIG: ' + title += "NON-RIG: " if item.dry_hire: - title += 'DRY HIRE: ' + title += "DRY HIRE: " # Add the rig name title += item.name # Add the status - title += f' ({item.get_status_display()})' + title += f" ({item.get_status_display()})" return title @@ -121,36 +135,54 @@ class CalendarICS(ICalFeed): tz = pytz.timezone(self.timezone) - desc = f'Rig ID = {item.display_id}\n' - desc += f'Event = {item.name}\n' - desc += 'Venue = ' + (item.venue.name if item.venue else '---') + '\n' + desc = f"Rig ID = {item.display_id}\n" + desc += f"Event = {item.name}\n" + desc += "Venue = " + (item.venue.name if item.venue else "---") + "\n" if item.is_rig and item.person: - desc += 'Client = ' + item.person.name + ( - (' for ' + item.organisation.name) if item.organisation else '') + '\n' - desc += f'Status = {item.get_status_display()}\n' - desc += 'MIC = ' + (item.mic.name if item.mic else '---') + '\n' + desc += ( + "Client = " + + item.person.name + + ((" for " + item.organisation.name) if item.organisation else "") + + "\n" + ) + desc += f"Status = {item.get_status_display()}\n" + desc += "MIC = " + (item.mic.name if item.mic else "---") + "\n" - desc += '\n' + desc += "\n" if item.meet_at: - desc += 'Crew Meet = ' + ( - item.meet_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.meet_at else '---') + '\n' + desc += ( + "Crew Meet = " + + (item.meet_at.astimezone(tz).strftime("%Y-%m-%d %H:%M") if item.meet_at else "---") + + "\n" + ) if item.access_at: - desc += 'Access At = ' + ( - item.access_at.astimezone(tz).strftime('%Y-%m-%d %H:%M') if item.access_at else '---') + '\n' + desc += ( + "Access At = " + + (item.access_at.astimezone(tz).strftime("%Y-%m-%d %H:%M") if item.access_at else "---") + + "\n" + ) if item.start_date: - desc += 'Event Start = ' + item.start_date.strftime('%Y-%m-%d') + ( - (' ' + item.start_time.strftime('%H:%M')) if item.has_start_time else '') + '\n' + desc += ( + "Event Start = " + + item.start_date.strftime("%Y-%m-%d") + + ((" " + item.start_time.strftime("%H:%M")) if item.has_start_time else "") + + "\n" + ) if item.end_date: - desc += 'Event End = ' + item.end_date.strftime('%Y-%m-%d') + ( - (' ' + item.end_time.strftime('%H:%M')) if item.has_end_time else '') + '\n' + desc += ( + "Event End = " + + item.end_date.strftime("%Y-%m-%d") + + ((" " + item.end_time.strftime("%H:%M")) if item.has_end_time else "") + + "\n" + ) - desc += '\n' + desc += "\n" if item.description: - desc += f'Event Description:\n{item.description}\n\n' + desc += f"Event Description:\n{item.description}\n\n" # if item.notes: // Need to add proper keyholder checks before this gets put back # desc += 'Notes:\n'+item.notes+'\n\n' - desc += f'URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}' + desc += f"URL = https://rigs.nottinghamtec.co.uk{item.get_absolute_url()}" return desc diff --git a/RIGS/views/rigboard.py b/RIGS/views/rigboard.py index 1c0daea6..dd48a9b4 100644 --- a/RIGS/views/rigboard.py +++ b/RIGS/views/rigboard.py @@ -8,7 +8,6 @@ import hmac import hashlib from envparse import env -from bs4 import BeautifulSoup from django.conf import settings from django.contrib import messages @@ -17,10 +16,9 @@ from django.core import signing from django.core.exceptions import SuspiciousOperation from django.core.mail import EmailMultiAlternatives from django.db.models import Q -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseForbidden from django.shortcuts import get_object_or_404 from django.template.loader import get_template -from django.urls import reverse from django.urls import reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator @@ -31,11 +29,11 @@ from PyRIGS import decorators from PyRIGS.views import OEmbedView, is_ajax, ModalURLMixin, PrintView, get_related from RIGS import models, forms -__author__ = 'ghost' +__author__ = "ghost" class RigboardIndex(generic.TemplateView): - template_name = 'rigboard.html' + template_name = "rigboard.html" def get_context_data(self, **kwargs): # get super context @@ -43,28 +41,28 @@ class RigboardIndex(generic.TemplateView): objects = models.Event.objects.current_events() - if self.request.GET.get('hide_cancelled', False): + if self.request.GET.get("hide_cancelled", False): objects = objects.exclude(status=models.Event.CANCELLED) # call out method to get current events - context['events'] = objects.select_related('riskassessment', 'invoice').prefetch_related('checklists') - context['page_title'] = "Rigboard" + context["events"] = objects.select_related("riskassessment", "invoice").prefetch_related("checklists") + context["page_title"] = "Rigboard" return context class WebCalendar(generic.TemplateView): - template_name = 'calendar.html' + template_name = "calendar.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['view'] = kwargs.get('view', '') - context['date'] = kwargs.get('date', '') + context["view"] = kwargs.get("view", "") + context["date"] = kwargs.get("date", "") # context['page_title'] = "Calendar" return context class EventDetail(generic.DetailView, ModalURLMixin): - template_name = 'event_detail.html' + template_name = "event_detail.html" model = models.Event def get_context_data(self, **kwargs): @@ -72,36 +70,36 @@ class EventDetail(generic.DetailView, ModalURLMixin): title = f"{self.object.display_id} | {self.object.name}" if self.object.dry_hire: title += " Dry Hire" - context['page_title'] = title + context["page_title"] = title if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" else: - context['override'] = 'base_assets.html' + context["override"] = "base_assets.html" return context class EventEmbed(EventDetail): - template_name = 'event_embed.html' + template_name = "event_embed.html" class EventOEmbed(OEmbedView): model = models.Event - url_name = 'event_embed' + url_name = "event_embed" class EventCreate(generic.CreateView): model = models.Event form_class = forms.EventForm - template_name = 'event_form.html' + template_name = "event_form.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = "New Event" - context['edit'] = True - context['currentVAT'] = models.VatRate.objects.current_rate() + context["page_title"] = "New Event" + context["edit"] = True + context["currentVAT"] = models.VatRate.objects.current_rate() - form = context['form'] - if hasattr(form, 'items_json') and re.search(r'"-\d+"', form['items_json'].value()): + form = context["form"] + if hasattr(form, "items_json") and re.search(r'"-\d+"', form["items_json"].value()): messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.") get_related(form, context) @@ -109,39 +107,43 @@ class EventCreate(generic.CreateView): return context def get_success_url(self): - return reverse_lazy('event_detail', kwargs={'pk': self.object.pk}) + return reverse_lazy("event_detail", kwargs={"pk": self.object.pk}) class EventUpdate(generic.UpdateView): model = models.Event form_class = forms.EventForm - template_name = 'event_form.html' + template_name = "event_form.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"Event {self.object.display_id}" - context['edit'] = True + context["page_title"] = f"Event {self.object.display_id}" + context["edit"] = True - form = context['form'] + form = context["form"] get_related(form, context) return context def render_to_response(self, context, **response_kwargs): - if hasattr(context, 'duplicate') and not context['duplicate']: + if hasattr(context, "duplicate") and not context["duplicate"]: # If this event has already been emailed to a client, show a warning if self.object.auth_request_at is not None: - messages.info(self.request, - 'This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.') + messages.info( + self.request, + "This event has already been sent to the client for authorisation, any changes you make will be visible to them immediately.", + ) - if hasattr(self.object, 'authorised'): - messages.warning(self.request, - 'This event has already been authorised by the client, any changes to the price will require reauthorisation.') + if hasattr(self.object, "authorised"): + messages.warning( + self.request, + "This event has already been authorised by the client, any changes to the price will require reauthorisation.", + ) return super().render_to_response(context, **response_kwargs) def get_success_url(self): - return reverse_lazy('event_detail', kwargs={'pk': self.object.pk}) + return reverse_lazy("event_detail", kwargs={"pk": self.object.pk}) class EventDuplicate(EventUpdate): @@ -158,34 +160,38 @@ class EventDuplicate(EventUpdate): new.collector = None # Remove all the authorisation information from the new event - new.auth_request_to = '' + new.auth_request_to = "" new.auth_request_by = None new.auth_request_at = None if self.request.method in ( - 'POST', 'PUT'): # This only happens on save (otherwise items won't display in editor) + "POST", + "PUT", + ): # This only happens on save (otherwise items won't display in editor) new.pk = None # This means a new event will be created on save, and all items will be re-created else: - messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.') + messages.info(self.request, "Event data duplicated but not yet saved. Click save to complete operation.") return new def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"Duplicate of Event {self.object.display_id}" + context["page_title"] = f"Duplicate of Event {self.object.display_id}" context["duplicate"] = True return context class EventPrint(PrintView): model = models.Event - template_name = 'event_print.xml' + template_name = "event_print.xml" append_terms = True def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['quote'] = True - context['filename'] = f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf" + context["quote"] = True + context["filename"] = ( + f"Event_{context['object'].display_id}_{context['object_name']}_{context['object'].start_date}.pdf" + ) return context @@ -196,20 +202,19 @@ class EventArchive(generic.ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['start'] = self.request.GET.get('start', None) - context['end'] = self.request.GET.get('end', datetime.date.today().strftime('%Y-%m-%d')) - context['statuses'] = models.Event.EVENT_STATUS_CHOICES - context['page_title'] = 'Event Archive' + context["start"] = self.request.GET.get("start", None) + context["end"] = self.request.GET.get("end", datetime.date.today().strftime("%Y-%m-%d")) + context["statuses"] = models.Event.EVENT_STATUS_CHOICES + context["page_title"] = "Event Archive" return context def get_queryset(self): - start = self.request.GET.get('start', None) - end = self.request.GET.get('end', datetime.date.today()) + start = self.request.GET.get("start", None) + end = self.request.GET.get("end", datetime.date.today()) # Assume idiots, always check if start and start > end: - messages.add_message(self.request, messages.INFO, - "Muppet! Check the dates, it has been fixed for you.") + messages.add_message(self.request, messages.INFO, "Muppet! Check the dates, it has been fixed for you.") start, end = end, start # Stop the impending fail filter = Q() @@ -218,21 +223,21 @@ class EventArchive(generic.ListView): if start: filter &= Q(start_date__gte=start) - q = self.request.GET.get('q', "") + q = self.request.GET.get("q", "") objects = self.model.objects.all() if q: objects = self.model.objects.search(q) - status = self.request.GET.getlist('status', "") + status = self.request.GET.getlist("status", "") if len(status) > 0: filter &= Q(status__in=status) - qs = objects.filter(filter).order_by('-start_date') + qs = objects.filter(filter).order_by("-start_date") # Preselect related for efficiency - qs.select_related('person', 'organisation', 'venue', 'mic') + qs.select_related("person", "organisation", "venue", "mic") if not qs.exists(): messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.") @@ -241,49 +246,57 @@ class EventArchive(generic.ListView): class EventAuthorise(generic.UpdateView): - template_name = 'eventauthorisation_form.html' - success_template = 'eventauthorisation_success.html' + template_name = "eventauthorisation_form.html" + success_template = "eventauthorisation_success.html" preview = False def form_valid(self, form): self.object = form.save() self.template_name = self.success_template - messages.add_message(self.request, messages.SUCCESS, - 'Success! Your event has been authorised. ' + - f'You will also receive email confirmation to {self.object.email}.') + messages.add_message( + self.request, + messages.SUCCESS, + "Success! Your event has been authorised. " + + f"You will also receive email confirmation to {self.object.email}.", + ) return self.render_to_response(self.get_context_data()) @property def event(self): - return models.Event.objects.select_related('organisation', 'person', 'venue').get(pk=self.kwargs['pk']) + return models.Event.objects.select_related("organisation", "person", "venue").get(pk=self.kwargs["pk"]) def get_object(self, queryset=None): - return getattr(self.event, 'authorisation', None) + return getattr(self.event, "authorisation", None) def get_form_class(self): return forms.InternalClientEventAuthorisationForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['event'] = self.event - context['tos_url'] = settings.TERMS_OF_HIRE_URL - context['page_title'] = f"{self.event.display_id}: {self.event.name}" + context["event"] = self.event + context["tos_url"] = settings.TERMS_OF_HIRE_URL + context["page_title"] = f"{self.event.display_id}: {self.event.name}" if self.event.dry_hire: - context['page_title'] += ' Dry Hire' - context['preview'] = self.preview + context["page_title"] += ' Dry Hire' + context["preview"] = self.preview return context def get(self, request, *args, **kwargs): if self.get_object() is not None and self.get_object().pk is not None: if self.event.authorised: - messages.add_message(self.request, messages.WARNING, - "This event has already been authorised. " - "Reauthorising is not necessary at this time.") + messages.add_message( + self.request, + messages.WARNING, + "This event has already been authorised. Reauthorising is not necessary at this time.", + ) else: - messages.add_message(self.request, messages.WARNING, - "This event has already been authorised, but the amount has changed. " + - "Please check the amount and reauthorise.") + messages.add_message( + self.request, + messages.WARNING, + "This event has already been authorised, but the amount has changed. " + + "Please check the amount and reauthorise.", + ) return super().get(request, *args, **kwargs) def get_form(self, **kwargs): @@ -296,20 +309,19 @@ class EventAuthorise(generic.UpdateView): def dispatch(self, request, *args, **kwargs): # Verify our signature matches up and all is well with the integrity of the URL try: - data = signing.loads(kwargs.get('hmac')) - assert int(kwargs.get('pk')) == int(data.get('pk')) - request.email = data['email'] - request.sent_by = models.Profile.objects.get(pk=data['sent_by']) - except (signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist): - raise SuspiciousOperation( - "This URL is invalid. Please ask your TEC contact for a new URL") + data = signing.loads(kwargs.get("hmac")) + assert int(kwargs.get("pk")) == int(data.get("pk")) + request.email = data["email"] + request.sent_by = models.Profile.objects.get(pk=data["sent_by"]) + except signing.BadSignature, AssertionError, KeyError, models.Profile.DoesNotExist: + raise SuspiciousOperation("This URL is invalid. Please ask your TEC contact for a new URL") return super().dispatch(request, *args, **kwargs) class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMixin): model = models.Event form_class = forms.EventAuthorisationRequestForm - template_name = 'eventauthorisation_request.html' + template_name = "eventauthorisation_request.html" @method_decorator(decorators.nottinghamtec_address_required) def dispatch(self, *args, **kwargs): @@ -321,17 +333,20 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix def get_success_url(self): if is_ajax(self.request): - url = reverse_lazy('closemodal') + url = reverse_lazy("closemodal") messages.info(self.request, "location.reload()") else: - url = reverse_lazy('event_detail', kwargs={ - 'pk': self.object.pk, - }) + url = reverse_lazy( + "event_detail", + kwargs={ + "pk": self.object.pk, + }, + ) messages.add_message(self.request, messages.SUCCESS, "Authorisation request successfully sent.") return url def form_valid(self, form): - email = form.cleaned_data['email'] + email = form.cleaned_data["email"] event = self.object event.auth_request_by = self.request.user event.auth_request_at = timezone.now() @@ -339,18 +354,20 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix event.save() context = { - 'object': self.object, - 'request': self.request, - 'hmac': signing.dumps({ - 'pk': self.object.pk, - 'email': email, - 'sent_by': self.request.user.pk, - }), + "object": self.object, + "request": self.request, + "hmac": signing.dumps( + { + "pk": self.object.pk, + "email": email, + "sent_by": self.request.user.pk, + } + ), } if event.person is not None and email == event.person.email: - context['to_name'] = event.person.name + context["to_name"] = event.person.name elif event.organisation is not None and email == event.organisation.email: - context['to_name'] = event.organisation.name + context["to_name"] = event.organisation.name msg = EmailMultiAlternatives( f"{self.object.display_id} | {self.object.name} - Event Authorisation Request", @@ -358,10 +375,13 @@ class EventAuthorisationRequest(generic.FormView, generic.detail.SingleObjectMix to=[email], reply_to=[self.request.user.email], ) - css = finders.find('css/email.css') - html = premailer.Premailer(get_template("email/eventauthorisation_client_request.html").render(context), - external_styles=css).transform() - msg.attach_alternative(html, 'text/html') + css = finders.find("css/email.css") + html = premailer.Premailer( + get_template("email/eventauthorisation_client_request.html").render(context), + external_styles=css, + allow_loading_external_files=True, + ).transform() + msg.attach_alternative(html, "text/html") msg.send() @@ -373,21 +393,25 @@ class EventAuthoriseRequestEmailPreview(generic.DetailView): model = models.Event def render_to_response(self, context, **response_kwargs): - css = finders.find('css/email.css') + css = finders.find("css/email.css") response = super().render_to_response(context, **response_kwargs) assert isinstance(response, HttpResponse) - response.content = premailer.Premailer(response.rendered_content, external_styles=css).transform() + response.content = premailer.Premailer( + response.rendered_content, external_styles=css, allow_loading_external_files=True + ).transform() return response def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['hmac'] = signing.dumps({ - 'pk': self.object.pk, - 'email': self.request.GET.get('email', 'hello@world.test'), - 'sent_by': self.request.user.pk, - }) - context['to_name'] = self.request.GET.get('to_name', None) - context['target'] = 'event_authorise_form_preview' + context["hmac"] = signing.dumps( + { + "pk": self.object.pk, + "email": self.request.GET.get("email", "hello@world.test"), + "sent_by": self.request.user.pk, + } + ) + context["to_name"] = self.request.GET.get("to_name", None) + context["target"] = "event_authorise_form_preview" return context @@ -395,17 +419,17 @@ class CreateForumThread(generic.base.RedirectView): permanent = False def get_redirect_url(self, *args, **kwargs): - event = get_object_or_404(models.Event, pk=kwargs['pk']) + event = get_object_or_404(models.Event, pk=kwargs["pk"]) if event.forum_url: return event.forum_url params = { - 'title': str(event), - 'body': f'https://rigs.nottinghamtec.co.uk/event/{event.pk}', - 'category': 'rig-info' + "title": str(event), + "body": f"https://rigs.nottinghamtec.co.uk/event/{event.pk}", + "category": "rig-info", } - return f'https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}' + return f"https://forum.nottinghamtec.co.uk/new-topic?{urllib.parse.urlencode(params)}" class RecieveForumWebhook(generic.View): @@ -415,12 +439,14 @@ class RecieveForumWebhook(generic.View): def post(self, request, *args, **kwargs): computed = f"sha256={hmac.new(env('FORUM_WEBHOOK_SECRET').encode(), request.body, hashlib.sha256).hexdigest()}" - if not hmac.compare_digest(request.headers.get('X-Discourse-Event-Signature'), computed): - return HttpResponseForbidden('Invalid signature header') + if not hmac.compare_digest(request.headers.get("X-Discourse-Event-Signature"), computed): + return HttpResponseForbidden("Invalid signature header") # Check if this is the right kind of event. The webhook filters by category on the forum side - if request.headers.get('X-Discourse-Event') == "topic_created": - body = simplejson.loads(request.body.decode('utf-8')) - event_id = int(body['topic']['title'][1:6]) # find the ID, force convert it to an int to eliminate leading zeros + if request.headers.get("X-Discourse-Event") == "topic_created": + body = simplejson.loads(request.body.decode("utf-8")) + event_id = int( + body["topic"]["title"][1:6] + ) # find the ID, force convert it to an int to eliminate leading zeros event = models.Event.objects.filter(pk=event_id).first() if event: event.forum_url = f"https://forum.nottinghamtec.co.uk/t/{body['topic']['slug']}" diff --git a/app.json b/app.json deleted file mode 100644 index 47bfc025..00000000 --- a/app.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "PyRIGS", - "description": "", - "scripts": { - "postdeploy": "python manage.py migrate && python manage.py generateSampleData" - }, - "stack": "heroku-22", - "env": { - "DEBUG": { - "required": true - }, - "STAGING": "1", - "EMAIL_FROM": { - "required": true - }, - "EMAIL_HOST": { - "required": true - }, - "EMAIL_HOST_PASSWORD": { - "required": true - }, - "EMAIL_HOST_USER": { - "required": true - }, - "EMAIL_PORT": { - "required": true - }, - "EMAIL_USE_SSL": { - "required": true - }, - "RECAPTCHA_PRIVATE_KEY": { - "required": true - }, - "RECAPTCHA_PUBLIC_KEY": { - "required": true - }, - "SECRET_KEY": { - "generator": "secret" - } - }, - "formation": { - "web": { - "quantity": 1 - } - }, - "addons": [ - "heroku-postgresql" - ], - "buildpacks": [ - { - "url": "heroku/nodejs" - }, - { - "url": "heroku/python" - } - ] -} diff --git a/assets/admin.py b/assets/admin.py index 51b548bb..bfbf9fa2 100644 --- a/assets/admin.py +++ b/assets/admin.py @@ -6,21 +6,21 @@ from assets import models as assets @admin.register(assets.AssetCategory) class AssetCategoryAdmin(admin.ModelAdmin): - list_display = ['id', 'name'] - ordering = ['id'] + list_display = ["id", "name"] + ordering = ["id"] @admin.register(assets.AssetStatus) class AssetStatusAdmin(admin.ModelAdmin): - list_display = ['id', 'name'] - ordering = ['id'] + list_display = ["id", "name"] + ordering = ["id"] @admin.register(assets.Supplier) class SupplierAdmin(AssociateAdmin): - list_display = ['id', 'name'] - ordering = ['id'] - merge_fields = ['name', 'phone', 'email', 'address', 'notes'] + list_display = ["id", "name"] + ordering = ["id"] + merge_fields = ["name", "phone", "email", "address", "notes"] def get_queryset(self, request): return super(VersionAdmin, self).get_queryset(request) @@ -28,16 +28,16 @@ class SupplierAdmin(AssociateAdmin): @admin.register(assets.Asset) class AssetAdmin(VersionAdmin): - list_display = ['id', 'asset_id', 'description', 'category', 'status'] - list_filter = ['is_cable', 'category', 'status'] - search_fields = ['id', 'asset_id', 'description'] + list_display = ["id", "asset_id", "description", "category", "status"] + list_filter = ["is_cable", "category", "status"] + search_fields = ["id", "asset_id", "description"] @admin.register(assets.CableType) class CableTypeAdmin(admin.ModelAdmin): - list_display = ['id', '__str__', 'plug', 'socket', 'cores', 'circuits'] + list_display = ["id", "__str__", "plug", "socket", "cores", "circuits"] @admin.register(assets.Connector) class ConnectorAdmin(admin.ModelAdmin): - list_display = ['id', '__str__', 'current_rating', 'voltage_rating', 'num_pins'] + list_display = ["id", "__str__", "current_rating", "voltage_rating", "num_pins"] diff --git a/assets/apps.py b/assets/apps.py index f9eaa84d..59d0a9ee 100644 --- a/assets/apps.py +++ b/assets/apps.py @@ -2,7 +2,7 @@ from django.apps import AppConfig class AssetsAppConfig(AppConfig): - name = 'assets' + name = "assets" def ready(self): - import assets.signals + import assets.signals # noqa: F401 (registers signal handlers) diff --git a/assets/converters.py b/assets/converters.py index e57c56bd..c1f67aea 100644 --- a/assets/converters.py +++ b/assets/converters.py @@ -1,8 +1,5 @@ -import urllib.parse - - class AssetIDConverter: # Forces lowercase to uppercase - regex = '[^/]+' + regex = "[^/]+" def to_python(self, value): return str(value).upper() @@ -12,10 +9,10 @@ class AssetIDConverter: # Forces lowercase to uppercase class ListConverter: - regex = '[^/]+' + regex = "[^/]+" def to_python(self, value): - return value.split(',') + return value.split(",") def to_url(self, value): string = "" diff --git a/assets/forms.py b/assets/forms.py index 0ccd32f3..bf933837 100644 --- a/assets/forms.py +++ b/assets/forms.py @@ -1,31 +1,35 @@ from django import forms -from django.db.models import Q from assets import models class AssetForm(forms.ModelForm): - related_models = { - 'asset': models.Asset, - 'supplier': models.Supplier - } + related_models = {"asset": models.Asset, "supplier": models.Supplier} class Meta: model = models.Asset - fields = '__all__' - exclude = ['asset_id_prefix', 'asset_id_number', 'last_audited_at', 'last_audited_by'] + fields = "__all__" + exclude = ["asset_id_prefix", "asset_id_number", "last_audited_at", "last_audited_by"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['date_sold'].widget.format = '%Y-%m-%d' - self.fields['date_acquired'].widget.format = '%Y-%m-%d' + self.fields["date_sold"].widget.format = "%Y-%m-%d" + self.fields["date_acquired"].widget.format = "%Y-%m-%d" class AssetAuditForm(AssetForm): class Meta(AssetForm.Meta): # Prevents assets losing existing data that isn't included in the audit form - exclude = ['asset_id_prefix', 'asset_id_number', 'last_audited_at', 'last_audited_by', - 'parent', 'purchased_from', 'purchase_price', 'comments'] + exclude = [ + "asset_id_prefix", + "asset_id_number", + "last_audited_at", + "last_audited_by", + "parent", + "purchased_from", + "purchase_price", + "comments", + ] class AssetSearchForm(forms.Form): @@ -40,10 +44,10 @@ class AssetSearchForm(forms.Form): class SupplierForm(forms.ModelForm): class Meta: model = models.Supplier - fields = '__all__' + fields = "__all__" class CableTypeForm(forms.ModelForm): class Meta: model = models.CableType - fields = '__all__' + fields = "__all__" diff --git a/assets/management/commands/generateSampleAssetsData.py b/assets/management/commands/generateSampleAssetsData.py index d9d66f16..7cf639e0 100644 --- a/assets/management/commands/generateSampleAssetsData.py +++ b/assets/management/commands/generateSampleAssetsData.py @@ -12,7 +12,7 @@ from assets.models import get_available_asset_id class Command(BaseCommand): - help = 'Creates some sample data for testing' + help = "Creates some sample data for testing" categories = [] statuses = [] @@ -25,9 +25,9 @@ class Command(BaseCommand): from django.conf import settings if not (settings.DEBUG or settings.STAGING): - raise CommandError('You cannot run this command in production') + raise CommandError("You cannot run this command in production") - random.seed('Some object to see the random number generator') + random.seed("Some object to see the random number generator") with transaction.atomic(): self.create_categories() @@ -39,23 +39,166 @@ class Command(BaseCommand): print("Done generating sample assets data") def create_categories(self): - choices = ['Case', 'Video', 'General', 'Sound', 'Lighting', 'Rigging'] + choices = ["Case", "Video", "General", "Sound", "Lighting", "Rigging"] for cat in choices: self.categories.append(models.AssetCategory.objects.create(name=cat)) def create_statuses(self): - choices = [('In Service', True, 'success'), ('Lost', False, 'warning'), ('Binned', False, 'danger'), ('Sold', False, 'danger'), ('Broken', False, 'warning')] + choices = [ + ("In Service", True, "success"), + ("Lost", False, "warning"), + ("Binned", False, "danger"), + ("Sold", False, "danger"), + ("Broken", False, "warning"), + ] for stat in choices: - self.statuses.append(models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2])) + self.statuses.append( + models.AssetStatus.objects.create(name=stat[0], should_show=stat[1], display_class=stat[2]) + ) def create_suppliers(self): - choices = ["Acme, inc.", "Widget Corp", "123 Warehousing", "Demo Company", "Smith and Co.", "Foo Bars", "ABC Telecom", "Fake Brothers", "QWERTY Logistics", "Demo, inc.", "Sample Company", "Sample, inc", "Acme Corp", "Allied Biscuit", "Ankh-Sto Associates", "Extensive Enterprise", "Galaxy Corp", "Globo-Chem", "Mr. Sparkle", "Globex Corporation", "LexCorp", "LuthorCorp", "North Central Positronics", "Omni Consimer Products", "Praxis Corporation", "Sombra Corporation", "Sto Plains Holdings", "Tessier-Ashpool", "Wayne Enterprises", "Wentworth Industries", "ZiffCorp", "Bluth Company", "Strickland Propane", "Thatherton Fuels", "Three Waters", "Water and Power", "Western Gas & Electric", "Mammoth Pictures", "Mooby Corp", "Gringotts", "Thrift Bank", "Flowers By Irene", "The Legitimate Businessmens Club", "Osato Chemicals", "Transworld Consortium", "Universal Export", "United Fried Chicken", "Virtucon", "Kumatsu Motors", "Keedsler Motors", "Powell Motors", "Industrial Automation", "Sirius Cybernetics Corporation", "U.S. Robotics and Mechanical Men", "Colonial Movers", "Corellian Engineering Corporation", "Incom Corporation", "General Products", "Leeding Engines Ltd.", "Blammo", # noqa - "Input, Inc.", "Mainway Toys", "Videlectrix", "Zevo Toys", "Ajax", "Axis Chemical Co.", "Barrytron", "Carrys Candles", "Cogswell Cogs", "Spacely Sprockets", "General Forge and Foundry", "Duff Brewing Company", "Dunder Mifflin", "General Services Corporation", "Monarch Playing Card Co.", "Krustyco", "Initech", "Roboto Industries", "Primatech", "Sonky Rubber Goods", "St. Anky Beer", "Stay Puft Corporation", "Vandelay Industries", "Wernham Hogg", "Gadgetron", "Burleigh and Stronginthearm", "BLAND Corporation", "Nordyne Defense Dynamics", "Petrox Oil Company", "Roxxon", "McMahon and Tate", "Sixty Second Avenue", "Charles Townsend Agency", "Spade and Archer", "Megadodo Publications", "Rouster and Sideways", "C.H. Lavatory and Sons", "Globo Gym American Corp", "The New Firm", "SpringShield", "Compuglobalhypermeganet", "Data Systems", "Gizmonic Institute", "Initrode", "Taggart Transcontinental", "Atlantic Northern", "Niagular", "Plow King", "Big Kahuna Burger", "Big T Burgers and Fries", "Chez Quis", "Chotchkies", "The Frying Dutchman", "Klimpys", "The Krusty Krab", "Monks Diner", "Milliways", "Minuteman Cafe", "Taco Grande", "Tip Top Cafe", "Moes Tavern", "Central Perk", "Chasers"] # noqa + choices = [ + "Acme, inc.", + "Widget Corp", + "123 Warehousing", + "Demo Company", + "Smith and Co.", + "Foo Bars", + "ABC Telecom", + "Fake Brothers", + "QWERTY Logistics", + "Demo, inc.", + "Sample Company", + "Sample, inc", + "Acme Corp", + "Allied Biscuit", + "Ankh-Sto Associates", + "Extensive Enterprise", + "Galaxy Corp", + "Globo-Chem", + "Mr. Sparkle", + "Globex Corporation", + "LexCorp", + "LuthorCorp", + "North Central Positronics", + "Omni Consimer Products", + "Praxis Corporation", + "Sombra Corporation", + "Sto Plains Holdings", + "Tessier-Ashpool", + "Wayne Enterprises", + "Wentworth Industries", + "ZiffCorp", + "Bluth Company", + "Strickland Propane", + "Thatherton Fuels", + "Three Waters", + "Water and Power", + "Western Gas & Electric", + "Mammoth Pictures", + "Mooby Corp", + "Gringotts", + "Thrift Bank", + "Flowers By Irene", + "The Legitimate Businessmens Club", + "Osato Chemicals", + "Transworld Consortium", + "Universal Export", + "United Fried Chicken", + "Virtucon", + "Kumatsu Motors", + "Keedsler Motors", + "Powell Motors", + "Industrial Automation", + "Sirius Cybernetics Corporation", + "U.S. Robotics and Mechanical Men", + "Colonial Movers", + "Corellian Engineering Corporation", + "Incom Corporation", + "General Products", + "Leeding Engines Ltd.", + "Blammo", # noqa + "Input, Inc.", + "Mainway Toys", + "Videlectrix", + "Zevo Toys", + "Ajax", + "Axis Chemical Co.", + "Barrytron", + "Carrys Candles", + "Cogswell Cogs", + "Spacely Sprockets", + "General Forge and Foundry", + "Duff Brewing Company", + "Dunder Mifflin", + "General Services Corporation", + "Monarch Playing Card Co.", + "Krustyco", + "Initech", + "Roboto Industries", + "Primatech", + "Sonky Rubber Goods", + "St. Anky Beer", + "Stay Puft Corporation", + "Vandelay Industries", + "Wernham Hogg", + "Gadgetron", + "Burleigh and Stronginthearm", + "BLAND Corporation", + "Nordyne Defense Dynamics", + "Petrox Oil Company", + "Roxxon", + "McMahon and Tate", + "Sixty Second Avenue", + "Charles Townsend Agency", + "Spade and Archer", + "Megadodo Publications", + "Rouster and Sideways", + "C.H. Lavatory and Sons", + "Globo Gym American Corp", + "The New Firm", + "SpringShield", + "Compuglobalhypermeganet", + "Data Systems", + "Gizmonic Institute", + "Initrode", + "Taggart Transcontinental", + "Atlantic Northern", + "Niagular", + "Plow King", + "Big Kahuna Burger", + "Big T Burgers and Fries", + "Chez Quis", + "Chotchkies", + "The Frying Dutchman", + "Klimpys", + "The Krusty Krab", + "Monks Diner", + "Milliways", + "Minuteman Cafe", + "Taco Grande", + "Tip Top Cafe", + "Moes Tavern", + "Central Perk", + "Chasers", + ] # noqa for supplier in choices: self.suppliers.append(models.Supplier.objects.create(name=supplier)) def create_assets(self): - asset_description = ['Large cable', 'Shiny thing', 'New lights', 'Really expensive microphone', 'Box of fuse flaps', 'Expensive tool we didn\'t agree to buy', 'Cable drums', 'Boring amount of tape', 'Video stuff no one knows how to use', 'More amplifiers', 'Heatshrink'] + asset_description = [ + "Large cable", + "Shiny thing", + "New lights", + "Really expensive microphone", + "Box of fuse flaps", + "Expensive tool we didn't agree to buy", + "Cable drums", + "Boring amount of tape", + "Video stuff no one knows how to use", + "More amplifiers", + "Heatshrink", + ] for i in range(100): with reversion.create_revision(): @@ -66,11 +209,11 @@ class Command(BaseCommand): description=random.choice(asset_description), category=random.choice(self.categories), status=random.choice(self.statuses), - date_acquired=timezone.now().date() + date_acquired=timezone.now().date(), ) if i % 4 == 0: - asset.parent = models.Asset.objects.order_by('?').first() + asset.parent = models.Asset.objects.order_by("?").first() if i % 3 == 0: asset.purchased_from = random.choice(self.suppliers) @@ -86,12 +229,18 @@ class Command(BaseCommand): {"description": "Socapex", "current_rating": 23, "voltage_rating": 600, "num_pins": 19}, ] for connector in connectors: - conn = models.Connector.objects.create(** connector) + conn = models.Connector.objects.create(**connector) conn.save() self.connectors.append(conn) def create_cables(self): - asset_description = ['The worm', 'Harting without a cap', 'Heavy cable', 'Extension lead', 'IEC cable that we should remember to prep'] + asset_description = [ + "The worm", + "Harting without a cap", + "Heavy cable", + "Extension lead", + "IEC cable that we should remember to prep", + ] asset_prefixes = ["C", "C4P", "CBNC", "CDMX", "CDV", "CRCD", "CSOCA", "CXLR"] csas = [0.75, 1.00, 1.25, 2.5, 4] @@ -101,7 +250,14 @@ class Command(BaseCommand): types = [] for i in range(len(self.connectors)): - types.append(models.CableType.objects.create(plug=random.choice(self.connectors), socket=random.choice(self.connectors), circuits=random.choice(circuits), cores=random.choice(cores))) + types.append( + models.CableType.objects.create( + plug=random.choice(self.connectors), + socket=random.choice(self.connectors), + circuits=random.choice(circuits), + cores=random.choice(cores), + ) + ) for i in range(100): prefix = random.choice(asset_prefixes) @@ -112,7 +268,6 @@ class Command(BaseCommand): category=random.choice(self.categories), status=random.choice(self.statuses), date_acquired=timezone.now().date(), - is_cable=True, cable_type=random.choice(types), csa=random.choice(csas), @@ -120,7 +275,7 @@ class Command(BaseCommand): ) if i % 4 == 0: - asset.parent = models.Asset.objects.order_by('?').first() + asset.parent = models.Asset.objects.order_by("?").first() if i % 3 == 0: asset.purchased_from = random.choice(self.suppliers) diff --git a/assets/migrations/0008_auto_20191206_2124.py b/assets/migrations/0008_auto_20191206_2124.py index f9002182..422badd7 100644 --- a/assets/migrations/0008_auto_20191206_2124.py +++ b/assets/migrations/0008_auto_20191206_2124.py @@ -12,7 +12,7 @@ def forwards(apps, schema_editor): asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id) if asset_search is None: # If the asset_id doesn't have a number at the end row.asset_id += "1" - + asset_search = re.search("^([A-Z0-9]*?[A-Z]?)([0-9]+)$", row.asset_id) row.asset_id_prefix = asset_search.group(1) row.asset_id_number = int(asset_search.group(2)) diff --git a/assets/migrations/0029_unique_constraints.py b/assets/migrations/0029_unique_constraints.py new file mode 100644 index 00000000..34099517 --- /dev/null +++ b/assets/migrations/0029_unique_constraints.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.17 on 2026-10-01 22:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0028_alter_asset_length'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='cabletype', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='cabletype', + constraint=models.UniqueConstraint(fields=('plug', 'socket', 'circuits', 'cores'), name='unique_plug_socket_circuits_cores'), + ), + ] diff --git a/assets/models.py b/assets/models.py index 3d8f70c8..d723d22c 100644 --- a/assets/models.py +++ b/assets/models.py @@ -1,11 +1,10 @@ import re from django.core.exceptions import ValidationError -from django.db import models, connection +from django.db import models from django.db.models import Q from django.urls import reverse from reversion import revisions as reversion -from reversion.models import Version from RIGS.models import Profile, ContactableManager from versioning.versioning import RevisionMixin @@ -15,9 +14,9 @@ class AssetCategory(models.Model): name = models.CharField(max_length=80) class Meta: - verbose_name = 'Asset Category' - verbose_name_plural = 'Asset Categories' - ordering = ['name'] + verbose_name = "Asset Category" + verbose_name_plural = "Asset Categories" + ordering = ["name"] def __str__(self): return self.name @@ -25,14 +24,17 @@ class AssetCategory(models.Model): class AssetStatus(models.Model): name = models.CharField(max_length=80) - should_show = models.BooleanField( - default=True, help_text="Should this be shown by default in the asset list.") - display_class = models.CharField(max_length=80, blank=True, help_text="HTML class to be appended to alter display of assets with this status, such as in the list.") + should_show = models.BooleanField(default=True, help_text="Should this be shown by default in the asset list.") + display_class = models.CharField( + max_length=80, + blank=True, + help_text="HTML class to be appended to alter display of assets with this status, such as in the list.", + ) class Meta: - verbose_name = 'Asset Status' - verbose_name_plural = 'Asset Statuses' - ordering = ['name'] + verbose_name = "Asset Status" + verbose_name_plural = "Asset Statuses" + ordering = ["name"] def __str__(self): return self.name @@ -50,10 +52,10 @@ class Supplier(models.Model, RevisionMixin): objects = ContactableManager() class Meta: - ordering = ['name'] + ordering = ["name"] def get_absolute_url(self): - return reverse('supplier_detail', kwargs={'pk': self.pk}) + return reverse("supplier_detail", kwargs={"pk": self.pk}) def __str__(self): return self.name @@ -61,8 +63,8 @@ class Supplier(models.Model, RevisionMixin): class Connector(models.Model): description = models.CharField(max_length=80) - current_rating = models.DecimalField(decimal_places=2, max_digits=10, help_text='Amps') - voltage_rating = models.IntegerField(help_text='Volts') + current_rating = models.DecimalField(decimal_places=2, max_digits=10, help_text="Amps") + voltage_rating = models.IntegerField(help_text="Volts") num_pins = models.IntegerField(blank=True, null=True) def __str__(self): @@ -72,14 +74,16 @@ class Connector(models.Model): class CableType(models.Model): circuits = models.IntegerField(default=1) cores = models.IntegerField(default=3) - plug = models.ForeignKey(Connector, on_delete=models.CASCADE, - related_name='plug') - socket = models.ForeignKey(Connector, on_delete=models.CASCADE, - related_name='socket') + plug = models.ForeignKey(Connector, on_delete=models.CASCADE, related_name="plug") + socket = models.ForeignKey(Connector, on_delete=models.CASCADE, related_name="socket") class Meta: - ordering = ['plug', 'socket', '-circuits'] - unique_together = ['plug', 'socket', 'circuits', 'cores'] + ordering = ["plug", "socket", "-circuits"] + constraints = [ + models.UniqueConstraint( + fields=["plug", "socket", "circuits", "cores"], name="unique_plug_socket_circuits_cores" + ) + ] def __str__(self): if self.plug and self.socket: @@ -88,14 +92,19 @@ class CableType(models.Model): return "Unknown" def get_absolute_url(self): - return reverse('cable_type_detail', kwargs={'pk': self.pk}) + return reverse("cable_type_detail", kwargs={"pk": self.pk}) class AssetManager(models.Manager): def search(self, query=None): qs = self.get_queryset() if query is not None: - or_lookup = (Q(asset_id__exact=query.upper()) | Q(description__icontains=query) | Q(serial_number__exact=query) | Q(nickname__icontains=query)) + or_lookup = ( + Q(asset_id__exact=query.upper()) + | Q(description__icontains=query) + | Q(serial_number__exact=query) + | Q(nickname__icontains=query) + ) qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups return qs @@ -113,53 +122,54 @@ def validate_positive(value): @reversion.register class Asset(models.Model, RevisionMixin): - parent = models.ForeignKey(to='self', related_name='asset_parent', - blank=True, null=True, on_delete=models.SET_NULL) + parent = models.ForeignKey(to="self", related_name="asset_parent", blank=True, null=True, on_delete=models.SET_NULL) asset_id = models.CharField(max_length=15, unique=True) description = models.CharField(max_length=120) category = models.ForeignKey(to=AssetCategory, on_delete=models.CASCADE) status = models.ForeignKey(to=AssetStatus, on_delete=models.CASCADE) serial_number = models.CharField(max_length=150, blank=True) - purchased_from = models.ForeignKey(to=Supplier, on_delete=models.SET_NULL, blank=True, null=True, related_name="assets") + purchased_from = models.ForeignKey( + to=Supplier, on_delete=models.SET_NULL, blank=True, null=True, related_name="assets" + ) date_acquired = models.DateField() date_sold = models.DateField(blank=True, null=True) - purchase_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive]) + purchase_price = models.DecimalField( + blank=True, null=True, decimal_places=2, max_digits=10, validators=[validate_positive] + ) replacement_cost = models.DecimalField(null=True, decimal_places=2, max_digits=10, validators=[validate_positive]) comments = models.TextField(blank=True) nickname = models.CharField(max_length=120, blank=True) # Audit last_audited_at = models.DateTimeField(blank=True, null=True) - last_audited_by = models.ForeignKey(Profile, on_delete=models.SET_NULL, related_name='audited_by', blank=True, null=True) + last_audited_by = models.ForeignKey( + Profile, on_delete=models.SET_NULL, related_name="audited_by", blank=True, null=True + ) # Cable assets is_cable = models.BooleanField(default=False) cable_type = models.ForeignKey(to=CableType, blank=True, null=True, on_delete=models.SET_NULL) - length = models.DecimalField(decimal_places=2, max_digits=10, - blank=True, null=True, help_text='m') - csa = models.DecimalField(decimal_places=2, max_digits=10, - blank=True, null=True, help_text='mm²') + length = models.DecimalField(decimal_places=2, max_digits=10, blank=True, null=True, help_text="m") + csa = models.DecimalField(decimal_places=2, max_digits=10, blank=True, null=True, help_text="mm²") # Hidden asset_id components # For example, if asset_id was "C1001" then asset_id_prefix would be "C" and number "1001" asset_id_prefix = models.CharField(max_length=8, default="") asset_id_number = models.IntegerField(default=1) - reversion_perm = 'assets.asset_finance' + reversion_perm = "assets.asset_finance" objects = AssetManager() class Meta: - ordering = ['asset_id_prefix', 'asset_id_number'] - permissions = [ - ('asset_finance', 'Can see financial data for assets') - ] + ordering = ["asset_id_prefix", "asset_id_number"] + permissions = [("asset_finance", "Can see financial data for assets")] def __str__(self): return f"{self.asset_id} | {self.description}" def get_absolute_url(self): - return reverse('asset_detail', kwargs={'pk': self.asset_id}) + return reverse("asset_detail", kwargs={"pk": self.asset_id}) def clean(self): errdict = {} @@ -169,8 +179,7 @@ class Asset(models.Model, RevisionMixin): self.asset_id = self.asset_id.upper() asset_search = re.search("^([a-zA-Z0-9]*?[a-zA-Z]?)([0-9]+)$", self.asset_id) if asset_search is None: - errdict["asset_id"] = [ - "An Asset ID can only consist of letters and numbers, with a final number"] + errdict["asset_id"] = ["An Asset ID can only consist of letters and numbers, with a final number"] if self.is_cable: if not self.length or self.length <= 0: diff --git a/assets/templates/asset_audit.html b/assets/templates/asset_audit.html index 83b584c0..a668ceab 100644 --- a/assets/templates/asset_audit.html +++ b/assets/templates/asset_audit.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_assets.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_assets.html' %} {% load widget_tweaks %} {% block content %} @@ -79,7 +79,7 @@
    - {% if not request.is_ajax %} + {% if not is_ajax %}
    diff --git a/assets/templates/asset_form.html b/assets/templates/asset_form.html index af61c8e4..b7c96e7b 100644 --- a/assets/templates/asset_form.html +++ b/assets/templates/asset_form.html @@ -1,4 +1,4 @@ -{% extends 'base_assets.html' %} +{% extends is_ajax|yesno:"base_ajax.html,base_assets.html" %} {% load widget_tweaks %} {% load static %} diff --git a/assets/templates/asset_update_search_results.html b/assets/templates/asset_update_search_results.html index edd156f1..46fcc57c 100644 --- a/assets/templates/asset_update_search_results.html +++ b/assets/templates/asset_update_search_results.html @@ -13,4 +13,4 @@ $('#parent_id').val(str); M.updateTextFields(); } - \ No newline at end of file + diff --git a/assets/templates/cable_type_detail.html b/assets/templates/cable_type_detail.html index c0e377c1..4023d546 100644 --- a/assets/templates/cable_type_detail.html +++ b/assets/templates/cable_type_detail.html @@ -37,4 +37,4 @@ {% endcache %}
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/assets/templatetags/asset_tags.py b/assets/templatetags/asset_tags.py index 553f646f..545fa6e2 100644 --- a/assets/templatetags/asset_tags.py +++ b/assets/templatetags/asset_tags.py @@ -1,5 +1,4 @@ from django import template -from assets import models register = template.Library() diff --git a/assets/tests/conftest.py b/assets/tests/conftest.py index f3f6d4ff..56d06ff3 100644 --- a/assets/tests/conftest.py +++ b/assets/tests/conftest.py @@ -19,7 +19,9 @@ def status(db): @pytest.fixture def cable_type(db): - connector = models.Connector.objects.create(description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3) + connector = models.Connector.objects.create( + description="16A IEC", current_rating=16, voltage_rating=240, num_pins=3 + ) cable_type = models.CableType.objects.create(circuits=11, cores=3, plug=connector, socket=connector) yield cable_type connector.delete() @@ -28,14 +30,33 @@ def cable_type(db): @pytest.fixture def test_cable(db, category, status, cable_type): - cable = models.Asset.objects.create(asset_id="9666", description="125A -> Jack", comments="The cable from Hell...", status=status, category=category, date_acquired=datetime.date(2006, 6, 6), is_cable=True, cable_type=cable_type, length=10, csa="1.5", replacement_cost=50) + cable = models.Asset.objects.create( + asset_id="9666", + description="125A -> Jack", + comments="The cable from Hell...", + status=status, + category=category, + date_acquired=datetime.date(2006, 6, 6), + is_cable=True, + cable_type=cable_type, + length=10, + csa="1.5", + replacement_cost=50, + ) yield cable cable.delete() @pytest.fixture def test_asset(db, category, status): - asset, created = models.Asset.objects.get_or_create(asset_id="91991", description="Spaceflower", status=status, category=category, date_acquired=datetime.date(1991, 12, 26), replacement_cost=100) + asset, created = models.Asset.objects.get_or_create( + asset_id="91991", + description="Spaceflower", + status=status, + category=category, + date_acquired=datetime.date(1991, 12, 26), + replacement_cost=100, + ) yield asset asset.delete() @@ -49,6 +70,20 @@ def test_status_2(db): @pytest.fixture def test_asset_2(db, category, test_status_2): - asset, created = models.Asset.objects.get_or_create(asset_id="10", description="Working Mic", status=test_status_2, category=category, date_acquired=datetime.date(2001, 10, 20), replacement_cost=1000) + asset, created = models.Asset.objects.get_or_create( + asset_id="10", + description="Working Mic", + status=test_status_2, + category=category, + date_acquired=datetime.date(2001, 10, 20), + replacement_cost=1000, + ) yield asset asset.delete() + + +@pytest.fixture +def test_supplier(db): + supplier, created = models.Supplier.objects.get_or_create(name="Fullmetal Heavy Industry") + yield supplier + supplier.delete() diff --git a/assets/tests/pages.py b/assets/tests/pages.py deleted file mode 100644 index 9f5afa32..00000000 --- a/assets/tests/pages.py +++ /dev/null @@ -1,251 +0,0 @@ -# Collection of page object models for use within tests. -from django.urls import reverse -from pypom import Region -from selenium.common.exceptions import NoSuchElementException -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions - -from PyRIGS.tests import regions -from PyRIGS.tests.pages import BasePage, FormPage, animation_is_finished - - -class AssetList(BasePage): - URL_TEMPLATE = '/assets/asset/list' - - _asset_item_locator = (By.CLASS_NAME, 'assetRow') - _search_text_locator = (By.ID, 'id_q') - _status_select_locator = (By.CSS_SELECTOR, 'div#status-group>div.bootstrap-select') - _category_select_locator = (By.CSS_SELECTOR, 'div#category-group>div.bootstrap-select') - _go_button_locator = (By.ID, 'id_search') - _filter_button_locator = (By.ID, 'filter-submit') - - class AssetListRow(Region): - _asset_id_locator = (By.CLASS_NAME, "assetID") - _asset_description_locator = (By.CLASS_NAME, "assetDesc") - _asset_category_locator = (By.CLASS_NAME, "assetCategory") - _asset_status_locator = (By.CLASS_NAME, "assetStatus") - - @property - def id(self): - return self.find_element(*self._asset_id_locator).text - - @property - def description(self): - return self.find_element(*self._asset_description_locator).text - - @property - def category(self): - return self.find_element(*self._asset_category_locator).text - - @property - def status(self): - return self.find_element(*self._asset_status_locator).text - - @property - def assets(self): - return [self.AssetListRow(self, i) for i in self.find_elements(*self._asset_item_locator)] - - @property - def query(self): - return self.find_element(*self._search_text_locator).text - - def set_query(self, queryString): - element = self.find_element(*self._search_text_locator) - element.clear() - element.send_keys(queryString) - - def search(self): - self.find_element(*self._go_button_locator).click() - - def filter(self): - self.find_element(*self._filter_button_locator).click() - - @property - def status_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._status_select_locator)) - - @property - def category_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._category_select_locator)) - - -class AssetForm(FormPage): - _purchased_from_select_locator = (By.XPATH, '//div[@id="purchased-from-group"]/div/div/div') - _parent_select_locator = (By.CSS_SELECTOR, 'div#parent-group>div.bootstrap-select') - form_items = { - 'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')), - 'description': (regions.TextBox, (By.ID, 'id_description')), - 'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')), - 'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')), - 'comments': (regions.TextBox, (By.ID, 'id_comments')), - 'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')), - 'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')), - 'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')), - 'date_sold': (regions.DatePicker, (By.ID, 'id_date_sold')), - 'category': (regions.SingleSelectPicker, (By.ID, 'id_category')), - 'status': (regions.SingleSelectPicker, (By.ID, 'id_status')), - - 'cable_type': (regions.SingleSelectPicker, (By.ID, 'id_cable_type')), - 'length': (regions.TextBox, (By.ID, 'id_length')), - 'csa': (regions.TextBox, (By.ID, 'id_csa')), - } - - @property - def purchased_from_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._purchased_from_select_locator)) - - @property - def parent_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._parent_select_locator)) - - -class AssetEdit(AssetForm): - URL_TEMPLATE = '/assets/asset/id/{asset_id}/edit/' - - @property - def success(self): - return '/edit' not in self.driver.current_url - - -class AssetCreate(AssetForm): - URL_TEMPLATE = '/assets/asset/create/' - - @property - def success(self): - return '/create' not in self.driver.current_url - - -class AssetDuplicate(AssetForm): - URL_TEMPLATE = '/assets/asset/id/{asset_id}/duplicate' - _submit_locator = (By.XPATH, "//button[@type='submit' and contains(., 'Duplicate')]") - - @property - def success(self): - return '/duplicate' not in self.driver.current_url - - -class SupplierList(BasePage): - URL_TEMPLATE = reverse('supplier_list') - - _supplier_item_locator = (By.ID, 'row_item') - _search_text_locator = (By.ID, 'id_search_text') - _go_button_locator = (By.ID, 'id_search') - - class SupplierListRow(Region): - _name_locator = (By.ID, "cell_name") - - @property - def name(self): - return self.find_element(*self._name_locator).text - - @property - def suppliers(self): - return [self.SupplierListRow(self, i) for i in self.find_elements(*self._supplier_item_locator)] - - @property - def query(self): - return self.find_element(*self._search_text_locator).text - - def set_query(self, queryString): - element = self.find_element(*self._search_text_locator) - element.clear() - element.send_keys(queryString) - - def search(self): - self.find_element(*self._go_button_locator).click() - - -class SupplierForm(FormPage): - form_items = { - 'name': (regions.TextBox, (By.ID, 'id_name')), - } - - -class SupplierCreate(SupplierForm): - URL_TEMPLATE = reverse('supplier_create') - - @property - def success(self): - return '/create' not in self.driver.current_url - - -class SupplierEdit(SupplierForm): - # TODO This should be using reverse - URL_TEMPLATE = '/assets/supplier/{supplier_id}/edit' - - @property - def success(self): - return '/edit' not in self.driver.current_url - - -class AssetAuditList(AssetList): - URL_TEMPLATE = reverse('asset_audit_list') - - _search_text_locator = (By.ID, 'id_q') - _go_button_locator = (By.ID, 'searchButton') - _modal_locator = (By.ID, 'modal') - _errors_selector = (By.CLASS_NAME, "alert-danger") - - @property - def modal(self): - return self.AssetAuditModal(self, self.find_element(*self._modal_locator)) - - @property - def query(self): - return self.find_element(*self._search_text_locator).text - - def set_query(self, query): - element = self.find_element(*self._search_text_locator) - element.clear() - element.send_keys(query) - - def search(self): - self.find_element(*self._go_button_locator).click() - self.wait.until(animation_is_finished()) - - @property - def error(self): - try: - return self.find_element(*self._errors_selector) - except NoSuchElementException: - return None - - class AssetAuditModal(regions.Modal): - _errors_selector = (By.CLASS_NAME, "alert-danger") - # Don't use the usual success selector - that tries and fails to hit the '10m long cable' helper button... - _submit_locator = (By.ID, "id_mark_audited") - _close_selector = (By.XPATH, "//button[@data-dismiss='modal']") - - form_items = { - 'asset_id': (regions.TextBox, (By.ID, 'id_asset_id')), - 'description': (regions.TextBox, (By.ID, 'id_description')), - 'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')), - 'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')), - 'replacement_cost': (regions.TextBox, (By.ID, 'id_replacement_cost')), - 'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')), - 'category': (regions.SingleSelectPicker, (By.ID, 'id_category')), - 'status': (regions.SingleSelectPicker, (By.ID, 'id_status')), - - 'plug': (regions.SingleSelectPicker, (By.ID, 'id_plug')), - 'socket': (regions.SingleSelectPicker, (By.ID, 'id_socket')), - 'length': (regions.TextBox, (By.ID, 'id_length')), - 'csa': (regions.TextBox, (By.ID, 'id_csa')), - 'circuits': (regions.TextBox, (By.ID, 'id_circuits')), - 'cores': (regions.TextBox, (By.ID, 'id_cores')) - } - - @property - def errors(self): - try: - error_page = regions.ErrorPage(self, self.find_element(*self._errors_selector)) - return error_page.errors - except NoSuchElementException: - return None - - def close(self): - self.page.find_element(*self._close_selector).click() - self.wait.until(expected_conditions.invisibility_of_element_located((By.ID, 'modal'))) - - def remove_all_required(self): - self.driver.execute_script("Array.from(document.getElementsByTagName(\"input\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});") - self.driver.execute_script("Array.from(document.getElementsByTagName(\"select\")).forEach(function (el, ind, arr) { el.removeAttribute(\"required\")});") diff --git a/assets/tests/test_interaction.py b/assets/tests/test_interaction.py deleted file mode 100644 index aff8423a..00000000 --- a/assets/tests/test_interaction.py +++ /dev/null @@ -1,358 +0,0 @@ -import time -import datetime -import pytest - -from django.utils import timezone -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as ec -from selenium.webdriver.support.ui import WebDriverWait - -from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_almost_equal -from PyRIGS.tests.pages import animation_is_finished -from assets import models -from . import pages - - -@screenshot_failure_cls -class TestAssetList(AutoLoginTest): - def setUp(self): - super().setUp() - sound = models.AssetCategory.objects.create(name="Sound") - lighting = models.AssetCategory.objects.create(name="Lighting") - - working = models.AssetStatus.objects.create(name="Working", should_show=True) - broken = models.AssetStatus.objects.create(name="Broken", should_show=False) - - models.Asset.objects.create(asset_id="1", description="Broken XLR", status=broken, category=sound, - date_acquired=datetime.date(2020, 2, 1)) - models.Asset.objects.create(asset_id="10", description="Working Mic", status=working, category=sound, - date_acquired=datetime.date(2020, 2, 1)) - models.Asset.objects.create(asset_id="2", description="A light", status=working, category=lighting, - date_acquired=datetime.date(2020, 2, 1)) - models.Asset.objects.create(asset_id="C1", description="The pearl", status=broken, category=lighting, - date_acquired=datetime.date(2020, 2, 1)) - self.page = pages.AssetList(self.driver, self.live_server_url).open() - - def test_default_statuses_applied(self): - # Only the working stuff should be shown initially - asset_descriptions = list(map(lambda x: x.description, self.page.assets)) - self.assertEqual(2, len(asset_descriptions)) - self.assertIn("A light", asset_descriptions) - self.assertIn("Working Mic", asset_descriptions) - - def test_asset_order(self): - # Only the working stuff should be shown initially - self.page.status_selector.open() - self.page.status_selector.set_option("Broken", True) - self.page.status_selector.close() - - self.page.search() - - asset_ids = list(map(lambda x: x.id, self.page.assets)) - self.assertEqual("1", asset_ids[0]) - self.assertEqual("2", asset_ids[1]) - self.assertEqual("10", asset_ids[2]) - self.assertEqual("C1", asset_ids[3]) - - -@pytest.mark.xfail(reason="Fails on CI for unknown reason", raises=AssertionError) -def test_search(logged_in_browser, admin_user, live_server, test_asset, test_asset_2, category, status, cable_type): - page = pages.AssetList(logged_in_browser.driver, live_server.url).open() - page.set_query(test_asset.asset_id) - page.search() - assert len(page.assets) == 1 - assert page.assets[0].description == test_asset.description - assert page.assets[0].id == test_asset.asset_id - - page.set_query(test_asset.description) - page.search() - assert len(page.assets) == 1 - assert page.assets[0].description == test_asset.description - - page.set_query("Random string") - page.search() - assert len(page.assets) == 0 - - page.set_query("") - page.search() - # Only working stuff shown by default - assert len(page.assets) == 1 - - page.status_selector.toggle() - assert page.status_selector.is_open - page.status_selector.select_all() - page.status_selector.toggle() - assert not page.status_selector.is_open - page.filter() - assert len(page.assets) == 2 - - page.category_selector.toggle() - assert page.category_selector.is_open - page.category_selector.set_option(category.name, True) - page.category_selector.close() - assert not page.category_selector.is_open - page.filter() - assert len(page.assets) == 2 - - -def test_cable_create(logged_in_browser, admin_user, live_server, test_asset, category, status, cable_type): - page = pages.AssetCreate(logged_in_browser.driver, live_server.url).open() - wait = WebDriverWait(logged_in_browser.driver, 20) - page.description = str(cable_type) - page.category = category.name - page.status = status.name - page.serial_number = "MELON-MELON-MELON" - page.comments = "You might need that" - page.replacement_cost = "666" - page.is_cable = True - - assert logged_in_browser.driver.find_element(By.ID, 'cable-table').is_displayed() - wait.until(animation_is_finished()) - page.cable_type = str(cable_type) - page.length = 10 - page.csa = "1.5" - - page.submit() - assert page.success - - -def test_asset_edit(logged_in_browser, admin_user, live_server, test_asset): - page = pages.AssetEdit(logged_in_browser.driver, live_server.url, asset_id=test_asset.asset_id).open() - - assert logged_in_browser.driver.find_element(By.ID, 'id_asset_id').get_attribute('readonly') is not None - - new_description = "Big Shelf" - page.description = new_description - - page.submit() - assert page.success - - assert models.Asset.objects.get(asset_id=test_asset.asset_id).description == new_description - - -def test_asset_duplicate(logged_in_browser, admin_user, live_server, test_asset): - page = pages.AssetDuplicate(logged_in_browser.driver, live_server.url, asset_id=test_asset.asset_id).open() - - assert test_asset.asset_id != page.asset_id - assert test_asset.description == page.description - assert test_asset.status.name == page.status - assert test_asset.category.name == page.category - assert test_asset.date_acquired == page.date_acquired.date() - - page.submit() - assert page.success - assert models.Asset.objects.last().description == test_asset.description - - -@screenshot_failure_cls -class TestAssetForm(AutoLoginTest): - def setUp(self): - super().setUp() - self.category = models.AssetCategory.objects.create(name="Health & Safety") - self.status = models.AssetStatus.objects.create(name="O.K.", should_show=True) - self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry") - self.parent = models.Asset.objects.create(asset_id="9000", description="Shelf", status=self.status, - category=self.category, date_acquired=datetime.date(2000, 1, 1)) - self.connector = models.Connector.objects.create(description="IEC", current_rating=10, voltage_rating=240, - num_pins=3) - self.cable_type = models.CableType.objects.create(plug=self.connector, socket=self.connector, circuits=1, - cores=3) - self.page = pages.AssetCreate(self.driver, self.live_server_url).open() - - def test_asset_create(self): - # Test that ID is automatically assigned and properly incremented - # self.assertIn(self.page.asset_id, "9001") FIXME - - self.page.remove_all_required() - self.page.asset_id = "XX$X" - self.page.submit() - self.assertFalse(self.page.success) - self.assertIn("An Asset ID can only consist of letters and numbers, with a final number", - self.page.errors["Asset id"]) - self.assertIn("This field is required.", self.page.errors["Description"]) - - self.page.open() - - self.page.description = desc = "Bodge Lead" - self.page.category = cat = "Health & Safety" - self.page.status = status = "O.K." - self.page.serial_number = sn = "0124567890-SAUSAGE" - self.page.comments = comments = "This is actually a sledgehammer, not a cable..." - - self.page.purchase_price = "12.99" - self.page.replacement_cost = "99.12" - self.page.date_acquired = acquired = datetime.date(2020, 5, 2) - self.page.purchased_from_selector.toggle() - self.assertTrue(self.page.purchased_from_selector.is_open) - self.page.purchased_from_selector.search(self.supplier.name[:-8]) - self.page.purchased_from_selector.set_option(self.supplier.name, True) - - self.page.parent_selector.toggle() - self.assertTrue(self.page.parent_selector.is_open) - option = self.parent.asset_id - self.page.parent_selector.search(option) - time.sleep(2) # Slow down for javascript - # self.page.parent_selector.set_option(option, True) - # self.assertTrue(self.page.parent_selector.options[0].selected) - self.page.parent_selector.toggle() - - self.assertFalse(self.driver.find_element(By.ID, 'cable-table').is_displayed()) - - self.page.submit() - self.assertTrue(self.page.success) - # Check that data is right - asset = models.Asset.objects.get(asset_id="9001") - self.assertEqual(asset.description, desc) - self.assertEqual(asset.category.name, cat) - self.assertEqual(asset.status.name, status) - self.assertEqual(asset.serial_number, sn) - self.assertEqual(asset.comments, comments) - # This one is important as it defaults to today's date - self.assertEqual(asset.date_acquired, acquired) - - -@screenshot_failure_cls -class TestSupplierList(AutoLoginTest): - def setUp(self): - super().setUp() - models.Supplier.objects.create(name="Fullmetal Heavy Industry") - models.Supplier.objects.create(name="Acme.") - models.Supplier.objects.create(name="TEC PA & Lighting") - models.Supplier.objects.create(name="Caterpillar Inc.") - models.Supplier.objects.create(name="N.E.R.D") - models.Supplier.objects.create(name="Khumalo") - models.Supplier.objects.create(name="1984 Incorporated") - self.page = pages.SupplierList(self.driver, self.live_server_url).open() - - # Should be sorted alphabetically - def test_order(self): - names = list(map(lambda x: x.name, self.page.suppliers)) - self.assertEqual("1984 Incorporated", names[0]) - self.assertEqual("Acme.", names[1]) - self.assertEqual("Caterpillar Inc.", names[2]) - self.assertEqual("Fullmetal Heavy Industry", names[3]) - self.assertEqual("Khumalo", names[4]) - self.assertEqual("N.E.R.D", names[5]) - self.assertEqual("TEC PA & Lighting", names[6]) - - def test_search(self): - self.page.set_query("TEC") - self.page.search() - - self.assertTrue(len(self.page.suppliers) == 1) - self.assertEqual("TEC PA & Lighting", self.page.suppliers[0].name) - - self.page.set_query("") - self.page.search() - time.sleep(1) - self.assertTrue(len(self.page.suppliers) == 7) - - self.page.set_query("This is not a supplier") - self.page.search() - self.assertTrue(len(self.page.suppliers) == 0) - - -@screenshot_failure_cls -class TestSupplierCreateAndEdit(AutoLoginTest): - def setUp(self): - super().setUp() - self.supplier = models.Supplier.objects.create(name="Fullmetal Heavy Industry") - - def test_supplier_create(self): - self.page = pages.SupplierCreate(self.driver, self.live_server_url).open() - - self.page.remove_all_required() - self.page.submit() - self.assertFalse(self.page.success) - self.assertIn("This field is required.", self.page.errors["Name"]) - - self.page.name = "Optican Health Supplies" - self.page.submit() - self.assertTrue(self.page.success) - - def test_supplier_edit(self): - self.page = pages.SupplierEdit(self.driver, self.live_server_url, supplier_id=self.supplier.pk).open() - - self.assertEqual("Fullmetal Heavy Industry", self.page.name) - new_name = "Cyberdyne Systems" - self.page.name = new_name - self.page.submit() - self.assertTrue(self.page.success) - - -def test_audit_search(logged_in_browser, live_server, test_asset): - page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open() - # Check that a failed search works - page.set_query("NOTFOUND") - page.search() - assert not logged_in_browser.find_by_id('modal').visible - logged_in_browser.driver.implicitly_wait(4) - assert logged_in_browser.is_text_present("Asset with that ID does not exist!") - - -def test_audit_success(logged_in_browser, admin_user, live_server, test_asset): - page = pages.AssetAuditList(logged_in_browser.driver, live_server.url).open() - wait = WebDriverWait(logged_in_browser.driver, 20) - page.set_query(test_asset.asset_id) - page.search() - wait.until(ec.visibility_of_element_located((By.ID, 'modal'))) - # Now do it properly - page.modal.description = new_desc = "A BIG hammer" - page.modal.submit() - logged_in_browser.driver.implicitly_wait(4) - wait.until(animation_is_finished()) - submit_time = timezone.now() - # Check data is correct - test_asset.refresh_from_db() - assert test_asset.description in new_desc - # Make sure audit 'log' was filled out - assert admin_user.initials == test_asset.last_audited_by.initials - assert_times_almost_equal(submit_time, test_asset.last_audited_at) - # Check we've removed it from the 'needing audit' list - assert test_asset.asset_id not in page.assets - - -@screenshot_failure_cls -class TestAssetAudit(AutoLoginTest): - def setUp(self): - super().setUp() - self.category = models.AssetCategory.objects.create(name="Haulage") - self.status = models.AssetStatus.objects.create(name="Probably Fine", should_show=True) - self.supplier = models.Supplier.objects.create(name="The Bazaar") - self.connector = models.Connector.objects.create(description="Trailer Socket", current_rating=1, - voltage_rating=40, num_pins=13) - models.Asset.objects.create(asset_id="1", description="Trailer Cable", status=self.status, - category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10) - models.Asset.objects.create(asset_id="11", description="Trailerboard", status=self.status, - category=self.category, date_acquired=datetime.date(2020, 2, 1), replacement_cost=10) - models.Asset.objects.create(asset_id="111", description="Erms", status=self.status, category=self.category, - date_acquired=datetime.date(2020, 2, 1), replacement_cost=10) - self.asset = models.Asset.objects.create(asset_id="1111", description="A hammer", status=self.status, - category=self.category, - date_acquired=datetime.date(2020, 2, 1), replacement_cost=10) - self.page = pages.AssetAuditList(self.driver, self.live_server_url).open() - self.wait = WebDriverWait(self.driver, 20) - - def test_audit_fail(self): - self.page.set_query(self.asset.asset_id) - self.page.search() - self.wait.until(ec.visibility_of_element_located((By.ID, 'modal'))) - # Do it wrong on purpose to check error display - self.page.modal.remove_all_required() - self.page.modal.description = "" - self.page.modal.submit() - self.wait.until(animation_is_finished()) - self.driver.implicitly_wait(4) - self.assertIn("This field is required.", self.page.modal.errors["Description"]) - - def test_audit_list(self): - self.assertEqual(models.Asset.objects.filter(last_audited_at=None).count(), len(self.page.assets)) - asset_row = self.page.assets[0] - self.driver.find_element(By.XPATH, "//a[contains(@class,'btn') and contains(., 'Audit')]").click() - self.wait.until(ec.visibility_of_element_located((By.ID, 'modal'))) - self.assertEqual(self.page.modal.asset_id, asset_row.id) - self.page.modal.close() - self.assertFalse(self.driver.find_element(By.ID, 'modal').is_displayed()) - # Make sure audit log was NOT filled out - audited = models.Asset.objects.get(asset_id=asset_row.id) - assert audited.last_audited_by is None diff --git a/assets/tests/test_unit.py b/assets/tests/test_unit.py index 2d4d94a9..9f9d9646 100644 --- a/assets/tests/test_unit.py +++ b/assets/tests/test_unit.py @@ -1,42 +1,39 @@ -import datetime - import pytest from django.urls import reverse -from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains +from pytest_django.asserts import assertFormError, assertRedirects from PyRIGS.tests.base import assert_oembed, login from assets import models -from django.utils import timezone pytestmark = pytest.mark.django_db def test_supplier_create(admin_client): - url = reverse('supplier_create') + url = reverse("supplier_create") response = admin_client.post(url) - assertFormError(response, 'form', 'name', 'This field is required.') + assertFormError(response.context["form"], "name", "This field is required.") def test_supplier_edit(admin_client): supplier = models.Supplier.objects.create(name="Gadgetron Corporation") - url = reverse('supplier_update', kwargs={'pk': supplier.pk}) - response = admin_client.post(url, {'name': ""}) - assertFormError(response, 'form', 'name', 'This field is required.') + url = reverse("supplier_update", kwargs={"pk": supplier.pk}) + response = admin_client.post(url, {"name": ""}) + assertFormError(response.context["form"], "name", "This field is required.") def test_404(admin_client): - urls = {'asset_detail', 'asset_update', 'asset_duplicate', 'supplier_detail', 'supplier_update'} + urls = {"asset_detail", "asset_update", "asset_duplicate", "supplier_detail", "supplier_update"} for url_name in urls: - request_url = reverse(url_name, kwargs={'pk': "0000"}) + request_url = reverse(url_name, kwargs={"pk": "0000"}) response = admin_client.get(request_url, follow=True) assert response.status_code == 404 def test_embed_login_redirect(client, django_user_model, test_asset): - request_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id}) - expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url) + request_url = reverse("asset_embed", kwargs={"pk": test_asset.asset_id}) + expected_url = "{0}?next={1}".format(reverse("login_embed"), request_url) # Request the page and check it redirects response = client.get(request_url, follow=True) @@ -51,14 +48,14 @@ def test_embed_login_redirect(client, django_user_model, test_asset): def test_login_cookie_warning(client, django_user_model): - login_url = reverse('login_embed') + login_url = reverse("login_embed") response = client.post(login_url, follow=True) assert "Cookies do not seem to be enabled" in str(response.content) def test_x_frame_headers(client, django_user_model, test_asset): - asset_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id}) - login_url = reverse('login_embed') + asset_url = reverse("asset_embed", kwargs={"pk": test_asset.asset_id}) + login_url = reverse("login_embed") login(client, django_user_model) @@ -73,58 +70,78 @@ def test_x_frame_headers(client, django_user_model, test_asset): def test_oembed(client, test_asset): client.logout() - asset_url = reverse('asset_detail', kwargs={'pk': test_asset.asset_id}) - asset_embed_url = reverse('asset_embed', kwargs={'pk': test_asset.asset_id}) - oembed_url = reverse('asset_oembed', kwargs={'pk': test_asset.asset_id}) + asset_url = reverse("asset_detail", kwargs={"pk": test_asset.asset_id}) + asset_embed_url = reverse("asset_embed", kwargs={"pk": test_asset.asset_id}) + oembed_url = reverse("asset_oembed", kwargs={"pk": test_asset.asset_id}) - alt_oembed_url = reverse('asset_oembed', kwargs={'pk': 999}) - alt_asset_embed_url = reverse('asset_embed', kwargs={'pk': 999}) + alt_oembed_url = reverse("asset_oembed", kwargs={"pk": 999}) + alt_asset_embed_url = reverse("asset_embed", kwargs={"pk": 999}) assert_oembed(alt_asset_embed_url, alt_oembed_url, client, asset_embed_url, asset_url, oembed_url) def test_asset_create(admin_client): - response = admin_client.post(reverse('asset_create'), {'date_sold': '2000-01-01', 'date_acquired': '2020-01-01', 'purchase_price': '-30', 'replacement_cost': '-30'}) - assertFormError(response, 'form', 'asset_id', 'This field is required.') + response = admin_client.post( + reverse("asset_create"), + {"date_sold": "2000-01-01", "date_acquired": "2020-01-01", "purchase_price": "-30", "replacement_cost": "-30"}, + ) + assertFormError( + response.context["form"], + "asset_id", + ["This field is required.", "An Asset ID can only consist of letters and numbers, with a final number"], + ) assert_asset_form_errors(response) def test_cable_create(admin_client): - response = admin_client.post(reverse('asset_create'), {'asset_id': 'X$%A', 'is_cable': True}) - assertFormError(response, 'form', 'asset_id', 'An Asset ID can only consist of letters and numbers, with a final number') - assertFormError(response, 'form', 'cable_type', 'A cable must have a type') - assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') - assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') + response = admin_client.post(reverse("asset_create"), {"asset_id": "X$%A", "is_cable": True}) + assertFormError( + response.context["form"], "asset_id", "An Asset ID can only consist of letters and numbers, with a final number" + ) + assertFormError(response.context["form"], "cable_type", "A cable must have a type") + assertFormError(response.context["form"], "length", "The length of a cable must be more than 0") + assertFormError(response.context["form"], "csa", "The CSA of a cable must be more than 0") def test_asset_edit(admin_client, test_asset): - url = reverse('asset_update', kwargs={'pk': test_asset.asset_id}) - response = admin_client.post(url, {'date_sold': '2000-12-01', 'date_acquired': '2020-12-01', 'purchase_price': '-50', 'replacement_cost': '-50', 'description': "", 'status': "", 'category': ""}) + url = reverse("asset_update", kwargs={"pk": test_asset.asset_id}) + response = admin_client.post( + url, + { + "date_sold": "2000-12-01", + "date_acquired": "2020-12-01", + "purchase_price": "-50", + "replacement_cost": "-50", + "description": "", + "status": "", + "category": "", + }, + ) assert_asset_form_errors(response) def test_cable_edit(admin_client, test_cable): - url = reverse('asset_update', kwargs={'pk': test_cable.asset_id}) - response = admin_client.post(url, {'is_cable': True, 'length': -3, 'csa': -3}) + url = reverse("asset_update", kwargs={"pk": test_cable.asset_id}) + response = admin_client.post(url, {"is_cable": True, "length": -3, "csa": -3}) # TODO Can't figure out how to select the 'none' option... - # assertFormError(response, 'form', 'cable_type', 'A cable must have a type') - assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') - assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') + # assertFormError(response.context['form'], 'cable_type', 'A cable must have a type') + assertFormError(response.context["form"], "length", "The length of a cable must be more than 0") + assertFormError(response.context["form"], "csa", "The CSA of a cable must be more than 0") def test_asset_duplicate(admin_client, test_cable): - url = reverse('asset_duplicate', kwargs={'pk': test_cable.asset_id}) - response = admin_client.post(url, {'is_cable': True, 'length': 0, 'csa': 0}) + url = reverse("asset_duplicate", kwargs={"pk": test_cable.asset_id}) + response = admin_client.post(url, {"is_cable": True, "length": 0, "csa": 0}) - assertFormError(response, 'form', 'length', 'The length of a cable must be more than 0') - assertFormError(response, 'form', 'csa', 'The CSA of a cable must be more than 0') + assertFormError(response.context["form"], "length", "The length of a cable must be more than 0") + assertFormError(response.context["form"], "csa", "The CSA of a cable must be more than 0") def assert_asset_form_errors(response): - assertFormError(response, 'form', 'description', 'This field is required.') - assertFormError(response, 'form', 'status', 'This field is required.') - assertFormError(response, 'form', 'category', 'This field is required.') - assertFormError(response, 'form', 'date_sold', 'Cannot sell an item before it is acquired') - assertFormError(response, 'form', 'purchase_price', 'A price cannot be negative') - assertFormError(response, 'form', 'replacement_cost', 'A price cannot be negative') + assertFormError(response.context["form"], "description", "This field is required.") + assertFormError(response.context["form"], "status", "This field is required.") + assertFormError(response.context["form"], "category", "This field is required.") + assertFormError(response.context["form"], "date_sold", "Cannot sell an item before it is acquired") + assertFormError(response.context["form"], "purchase_price", "A price cannot be negative") + assertFormError(response.context["form"], "replacement_cost", "A price cannot be negative") diff --git a/assets/urls.py b/assets/urls.py index 3091b84b..5ecaf353 100644 --- a/assets/urls.py +++ b/assets/urls.py @@ -3,44 +3,73 @@ from django.urls import path, register_converter from django.views.decorators.clickjacking import xframe_options_exempt from PyRIGS.decorators import has_oembed, permission_required_with_403 -from PyRIGS.views import OEmbedView from . import views, converters -register_converter(converters.AssetIDConverter, 'asset') -register_converter(converters.ListConverter, 'list') +register_converter(converters.AssetIDConverter, "asset") +register_converter(converters.ListConverter, "list") urlpatterns = [ - path('', login_required(views.AssetList.as_view()), name='asset_index'), - path('asset/list/', login_required(views.AssetList.as_view()), name='asset_list'), - path('asset/id//', has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name='asset_detail'), - path('asset/create/', permission_required_with_403('assets.add_asset') - (views.AssetCreate.as_view()), name='asset_create'), - path('asset/id//edit/', permission_required_with_403('assets.change_asset') - (views.AssetEdit.as_view()), name='asset_update'), - path('asset/id//duplicate/', permission_required_with_403('assets.add_asset') - (views.AssetDuplicate.as_view()), name='asset_duplicate'), - path('asset/id//label', login_required(views.GenerateLabel.as_view()), name='generate_label'), - path('asset//list/label', views.GenerateLabels.as_view(), name='generate_labels'), - - path('cables/list/', login_required(views.CableList.as_view()), name='cable_list'), - path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'), - path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'), - path('cabletype//update/', permission_required_with_403('assets.change_cable_type')(views.CableTypeUpdate.as_view()), name='cable_type_update'), - path('cabletype//detail/', login_required(views.CableTypeDetail.as_view()), name='cable_type_detail'), - - path('asset/id//embed/', - xframe_options_exempt( - login_required(login_url='/user/login/embed/')(views.AssetEmbed.as_view())), - name='asset_embed'), - path('asset/id//oembed_json/', views.AssetOEmbed.as_view(), name='asset_oembed'), - - path('asset/audit/', permission_required_with_403('assets.change_asset')(views.AssetAuditList.as_view()), name='asset_audit_list'), - path('asset/id//audit/', permission_required_with_403('assets.change_asset')(views.AssetAudit.as_view()), name='asset_audit'), - - path('supplier/list/', login_required(views.SupplierList.as_view()), name='supplier_list'), - path('supplier//', login_required(views.SupplierDetail.as_view()), name='supplier_detail'), - path('supplier/create/', permission_required_with_403('assets.add_supplier') - (views.SupplierCreate.as_view()), name='supplier_create'), - path('supplier//edit/', permission_required_with_403('assets.change_supplier') - (views.SupplierUpdate.as_view()), name='supplier_update'), + path("", login_required(views.AssetList.as_view()), name="asset_index"), + path("asset/list/", login_required(views.AssetList.as_view()), name="asset_list"), + path( + "asset/id//", has_oembed(oembed_view="asset_oembed")(views.AssetDetail.as_view()), name="asset_detail" + ), + path( + "asset/create/", + permission_required_with_403("assets.add_asset")(views.AssetCreate.as_view()), + name="asset_create", + ), + path( + "asset/id//edit/", + permission_required_with_403("assets.change_asset")(views.AssetEdit.as_view()), + name="asset_update", + ), + path( + "asset/id//duplicate/", + permission_required_with_403("assets.add_asset")(views.AssetDuplicate.as_view()), + name="asset_duplicate", + ), + path("asset/id//label", login_required(views.GenerateLabel.as_view()), name="generate_label"), + path("asset//list/label", views.GenerateLabels.as_view(), name="generate_labels"), + path("cables/list/", login_required(views.CableList.as_view()), name="cable_list"), + path("cabletype/list/", login_required(views.CableTypeList.as_view()), name="cable_type_list"), + path( + "cabletype/create/", + permission_required_with_403("assets.add_cable_type")(views.CableTypeCreate.as_view()), + name="cable_type_create", + ), + path( + "cabletype//update/", + permission_required_with_403("assets.change_cable_type")(views.CableTypeUpdate.as_view()), + name="cable_type_update", + ), + path("cabletype//detail/", login_required(views.CableTypeDetail.as_view()), name="cable_type_detail"), + path( + "asset/id//embed/", + xframe_options_exempt(login_required(login_url="/user/login/embed/")(views.AssetEmbed.as_view())), + name="asset_embed", + ), + path("asset/id//oembed_json/", views.AssetOEmbed.as_view(), name="asset_oembed"), + path( + "asset/audit/", + permission_required_with_403("assets.change_asset")(views.AssetAuditList.as_view()), + name="asset_audit_list", + ), + path( + "asset/id//audit/", + permission_required_with_403("assets.change_asset")(views.AssetAudit.as_view()), + name="asset_audit", + ), + path("supplier/list/", login_required(views.SupplierList.as_view()), name="supplier_list"), + path("supplier//", login_required(views.SupplierDetail.as_view()), name="supplier_detail"), + path( + "supplier/create/", + permission_required_with_403("assets.add_supplier")(views.SupplierCreate.as_view()), + name="supplier_create", + ), + path( + "supplier//edit/", + permission_required_with_403("assets.change_supplier")(views.SupplierUpdate.as_view()), + name="supplier_update", + ), ] diff --git a/assets/views.py b/assets/views.py index d1eb3396..d7127d97 100644 --- a/assets/views.py +++ b/assets/views.py @@ -1,5 +1,3 @@ -import simplejson -import random import base64 from io import BytesIO @@ -7,38 +5,43 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.core import serializers from django.db.models import Q, Sum -from django.http import Http404, HttpResponse, JsonResponse +from django.http import HttpResponse from django.urls import reverse, reverse_lazy from django.utils import timezone -from django.utils.decorators import method_decorator from django.views import generic -from django.views.decorators.csrf import csrf_exempt from django.shortcuts import get_object_or_404 from django.template.loader import get_template -from PyPDF2 import PdfFileMerger, PdfFileReader +from pypdf import PdfReader, PdfWriter from PIL import Image, ImageDraw, ImageFont, ImageOps from barcode import Code39 from barcode.writer import ImageWriter from z3c.rml import rml2pdf -from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \ - is_ajax, OEmbedView +from PyRIGS.views import ( + GenericListView, + GenericDetailView, + GenericUpdateView, + GenericCreateView, + ModalURLMixin, + is_ajax, + OEmbedView, +) from assets import forms, models class AssetList(LoginRequiredMixin, generic.ListView): model = models.Asset - template_name = 'asset_list.html' + template_name = "asset_list.html" paginate_by = 40 hide_hidden_status = True def get_initial(self): - initial = {'status': models.AssetStatus.objects.filter(should_show=True)} + initial = {"status": models.AssetStatus.objects.filter(should_show=True)} return initial def get_queryset(self): - if self.request.method == 'GET' and len(self.request.GET) > 0: + if self.request.method == "GET" and len(self.request.GET) > 0: self.form = forms.AssetSearchForm(data=self.request.GET) else: self.form = forms.AssetSearchForm(data=self.get_initial()) @@ -47,32 +50,31 @@ class AssetList(LoginRequiredMixin, generic.ListView): return self.model.objects.none() # TODO Feedback to user when search fails - query_string = form.cleaned_data['q'] or "" + query_string = form.cleaned_data["q"] or "" queryset = models.Asset.objects.search(query=query_string) - if form.cleaned_data['is_cable']: + if form.cleaned_data["is_cable"]: queryset = queryset.filter(is_cable=True) - if form.cleaned_data['date_acquired']: - queryset = queryset.filter(date_acquired=form.cleaned_data['date_acquired']) + if form.cleaned_data["date_acquired"]: + queryset = queryset.filter(date_acquired=form.cleaned_data["date_acquired"]) - if form.cleaned_data['category']: - queryset = queryset.filter(category__in=form.cleaned_data['category']) + if form.cleaned_data["category"]: + queryset = queryset.filter(category__in=form.cleaned_data["category"]) - if len(form.cleaned_data['status']) > 0: - queryset = queryset.filter(status__in=form.cleaned_data['status']) + if len(form.cleaned_data["status"]) > 0: + queryset = queryset.filter(status__in=form.cleaned_data["status"]) elif self.hide_hidden_status: - queryset = queryset.filter( - status__in=models.AssetStatus.objects.filter(should_show=True)) + queryset = queryset.filter(status__in=models.AssetStatus.objects.filter(should_show=True)) - return queryset.select_related('category', 'status') + return queryset.select_related("category", "status") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["form"] = self.form - if hasattr(self.form, 'cleaned_data'): - context["category_filters"] = self.form.cleaned_data.get('category') - context["status_filters"] = self.form.cleaned_data.get('status') + if hasattr(self.form, "cleaned_data"): + context["category_filters"] = self.form.cleaned_data.get("category") + context["status_filters"] = self.form.cleaned_data.get("status") context["categories"] = models.AssetCategory.objects.all() context["statuses"] = models.AssetStatus.objects.all() context["page_title"] = "Asset List" @@ -80,21 +82,21 @@ class AssetList(LoginRequiredMixin, generic.ListView): class CableList(AssetList): - template_name = 'cable_list.html' + template_name = "cable_list.html" paginator = None def get_queryset(self): queryset = super().get_queryset().filter(is_cable=True) - if self.form.cleaned_data['cable_type']: - queryset = queryset.filter(cable_type__in=self.form.cleaned_data['cable_type']) + if self.form.cleaned_data["cable_type"]: + queryset = queryset.filter(cable_type__in=self.form.cleaned_data["cable_type"]) return queryset def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_title"] = "Cable List" - context["total_length"] = self.get_queryset().aggregate(Sum('length'))['length__sum'] + context["total_length"] = self.get_queryset().aggregate(Sum("length"))["length__sum"] return context @@ -106,7 +108,7 @@ class AssetIDUrlMixin: class AssetDetail(LoginRequiredMixin, AssetIDUrlMixin, generic.DetailView): model = models.Asset - template_name = 'asset_detail.html' + template_name = "asset_detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -115,7 +117,7 @@ class AssetDetail(LoginRequiredMixin, AssetIDUrlMixin, generic.DetailView): class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView): - template_name = 'asset_form.html' + template_name = "asset_form.html" model = models.Asset form_class = forms.AssetForm @@ -128,17 +130,22 @@ class AssetEdit(LoginRequiredMixin, AssetIDUrlMixin, generic.UpdateView): def get_success_url(self): if is_ajax(self.request): - url = reverse_lazy('closemodal') - update_url = str(reverse_lazy('asset_update', kwargs={'pk': self.object.pk})) + url = reverse_lazy("closemodal") + update_url = str(reverse_lazy("asset_update", kwargs={"pk": self.object.pk})) messages.info(self.request, "modalobject=" + serializers.serialize("json", [self.object])) messages.info(self.request, "modalobject[0]['update_url']='" + update_url + "'") else: - url = reverse_lazy('asset_detail', kwargs={'pk': self.object.asset_id, }) + url = reverse_lazy( + "asset_detail", + kwargs={ + "pk": self.object.asset_id, + }, + ) return url class AssetCreate(LoginRequiredMixin, generic.CreateView): - template_name = 'asset_form.html' + template_name = "asset_form.html" model = models.Asset form_class = forms.AssetForm @@ -176,37 +183,37 @@ class AssetDuplicate(DuplicateMixin, AssetIDUrlMixin, AssetCreate): context["create"] = None context["duplicate"] = True old_id = self.get_object().asset_id - context['previous_asset_id'] = old_id + context["previous_asset_id"] = old_id context["page_title"] = f"Duplication of Asset: {old_id}" return context class AssetEmbed(AssetDetail): - template_name = 'asset_embed.html' + template_name = "asset_embed.html" class AssetOEmbed(OEmbedView): model = models.Asset - url_name = 'asset_embed' + url_name = "asset_embed" class AssetAuditList(AssetList): - template_name = 'asset_audit_list.html' + template_name = "asset_audit_list.html" hide_hidden_status = True # TODO Refresh this when the modal is submitted def get_queryset(self): self.form = forms.AssetSearchForm(data=self.request.GET) - return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related('category', 'status') + return self.model.objects.filter(Q(last_audited_at__isnull=True)).select_related("category", "status") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = "Asset Audit List" + context["page_title"] = "Asset Audit List" return context class AssetAudit(AssetEdit): - template_name = 'asset_audit.html' + template_name = "asset_audit.html" form_class = forms.AssetAuditForm def get_context_data(self, **kwargs): @@ -225,18 +232,18 @@ class AssetAudit(AssetEdit): class SupplierList(GenericListView): model = models.Supplier - ordering = ['name'] + ordering = ["name"] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['create'] = 'supplier_create' - context['edit'] = 'supplier_update' - context['can_edit'] = self.request.user.has_perm('assets.change_supplier') - context['detail'] = 'supplier_detail' + context["create"] = "supplier_create" + context["edit"] = "supplier_update" + context["can_edit"] = self.request.user.has_perm("assets.change_supplier") + context["detail"] = "supplier_detail" if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" else: - context['override'] = 'base_assets.html' + context["override"] = "base_assets.html" return context @@ -245,16 +252,16 @@ class SupplierDetail(GenericDetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['history_link'] = 'supplier_history' - context['update_link'] = 'supplier_update' - context['detail_link'] = 'supplier_detail' - context['associated'] = 'partials/associated_assets.html' - context['associated2'] = '' + context["history_link"] = "supplier_history" + context["update_link"] = "supplier_update" + context["detail_link"] = "supplier_detail" + context["associated"] = "partials/associated_assets.html" + context["associated2"] = "" if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" else: - context['override'] = 'base_assets.html' - context['can_edit'] = self.request.user.has_perm('assets.change_supplier') + context["override"] = "base_assets.html" + context["can_edit"] = self.request.user.has_perm("assets.change_supplier") return context @@ -265,13 +272,13 @@ class SupplierCreate(GenericCreateView, ModalURLMixin): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" else: - context['override'] = 'base_assets.html' + context["override"] = "base_assets.html" return context def get_success_url(self): - return self.get_close_url('supplier_update', 'supplier_detail') + return self.get_close_url("supplier_update", "supplier_detail") class SupplierUpdate(GenericUpdateView, ModalURLMixin): @@ -281,22 +288,22 @@ class SupplierUpdate(GenericUpdateView, ModalURLMixin): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" else: - context['override'] = 'base_assets.html' + context["override"] = "base_assets.html" return context def get_success_url(self): - return self.get_close_url('supplier_update', 'supplier_detail') + return self.get_close_url("supplier_update", "supplier_detail") class CableTypeList(generic.ListView): model = models.CableType - template_name = 'cable_type_list.html' + template_name = "cable_type_list.html" paginate_by = 40 def get_queryset(self): - return self.model.objects.select_related('plug', 'socket') + return self.model.objects.select_related("plug", "socket") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -306,7 +313,7 @@ class CableTypeList(generic.ListView): class CableTypeDetail(generic.DetailView): model = models.CableType - template_name = 'cable_type_detail.html' + template_name = "cable_type_detail.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -391,8 +398,8 @@ class GenerateLabel(generic.View): # TODO Caching class GenerateLabels(generic.View): def get(self, request, ids): - response = HttpResponse(content_type='application/pdf') - template = get_template('labels_print.xml') + response = HttpResponse(content_type="application/pdf") + template = get_template("labels_print.xml") images = [] @@ -405,28 +412,28 @@ class GenerateLabels(generic.View): img_bytes = in_mem_file.read() base64_encoded_result_bytes = base64.b64encode(img_bytes) - base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii') + base64_encoded_result_str = base64_encoded_result_bytes.decode("ascii") images.append((get_object_or_404(models.Asset, asset_id=asset_id), base64_encoded_result_str)) name = f"Asset Label Sheet generated at {timezone.now()}" context = { - 'images0': images[::3], - 'images1': images[1::3], - 'images2': images[2::3], + "images0": images[::3], + "images1": images[1::3], + "images2": images[2::3], # 'images3': images[3::4], - 'filename': name + "filename": name, } - merger = PdfFileMerger() + merger = PdfWriter() rml = template.render(context) buffer = rml2pdf.parseString(rml) - merger.append(PdfFileReader(buffer)) + merger.append(PdfReader(buffer)) buffer.close() merged = BytesIO() merger.write(merged) - response['Content-Disposition'] = f'filename="{name}"' + response["Content-Disposition"] = f'filename="{name}"' response.write(merged.getvalue()) return response diff --git a/compose.yml b/compose.yml new file mode 100644 index 00000000..50769eb8 --- /dev/null +++ b/compose.yml @@ -0,0 +1,110 @@ +services: + db: + image: postgres:18.6 + restart: unless-stopped + environment: + POSTGRES_DB: ${DATABASE_NAME} + POSTGRES_USER: ${DATABASE_USERNAME} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + # postgres 18+ defaults PGDATA to a versioned subdirectory; pin it so the volume below holds the data + PGDATA: /var/lib/postgresql/data + + volumes: + - postgres_data:/var/lib/postgresql/data + + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}" ] + interval: 10s + timeout: 5s + retries: 5 + + pyrigs: + build: . + restart: unless-stopped + environment: + SECRET_KEY: ${SECRET_KEY} + DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS} + DOMAIN: ${DOMAIN:-example.com} + DEBUG: ${DEBUG:-false} + STAGING: ${STAGING:-false} + SESSION_COOKIE_SECURE_ENABLED: ${SESSION_COOKIE_SECURE_ENABLED:-true} + CSRF_COOKIE_SECURE_ENABLED: ${CSRF_COOKIE_SECURE_ENABLED:-true} + FORUM_WEBHOOK_SECRET: ${FORUM_WEBHOOK_SECRET} + DATABASE_ENGINE: ${DATABASE_ENGINE:-postgresql} + DATABASE_NAME: ${DATABASE_NAME} + DATABASE_USERNAME: ${DATABASE_USERNAME} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + DATABASE_HOST: ${DATABASE_HOST:-db} + DATABASE_PORT: ${DATABASE_PORT:-5432} + EMAIL_HOST: ${EMAIL_HOST} + EMAIL_PORT: ${EMAIL_PORT:-25} + EMAIL_HOST_USER: ${EMAIL_HOST_USER} + EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} + EMAIL_USE_TLS: ${EMAIL_USE_TLS:-false} + EMAIL_USE_SSL: ${EMAIL_USE_SSL:-false} + EMAIL_FROM: ${EMAIL_FROM} + HCAPTCHA_SITEKEY: ${HCAPTCHA_SITEKEY:-10000000-ffff-ffff-ffff-000000000001} + HCAPTCHA_SECRET: ${HCAPTCHA_SECRET:-0x0000000000000000000000000000000000000000} + SENTRY_DSN: ${SENTRY_DSN:-} + expose: + - "8000" + + healthcheck: + # gunicorn only starts listening once migrations and collectstatic have finished + test: [ "CMD", "python", "-c", "import socket; socket.create_connection(('127.0.0.1', 8000), 2).close()" ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 60s + + volumes: + # shared with nginx, which serves /static/ directly + - static_files:/app/static + + depends_on: + db: + condition: service_healthy + + command: + - sh + - -c + - | + python manage.py migrate --noinput && + python manage.py collectstatic --noinput && + gunicorn \ + --bind 0.0.0.0:8000 \ + --workers 2 \ + PyRIGS.wsgi + + develop: + # Create a `watch` configuration to update the app + watch: + # Sync the working directory with the `/app` directory in the container + - action: sync + path: . + target: /app + # Exclude the project virtual environment + ignore: + - .venv/ + + # Rebuild the image on changes to the `pyproject.toml` + - action: rebuild + path: ./pyproject.toml + + nginx: + image: nginx:stable-alpine + restart: unless-stopped + ports: + - "80:80" + + volumes: + - ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro + - static_files:/var/www/static:ro + + depends_on: + pyrigs: + condition: service_healthy + +volumes: + postgres_data: + static_files: diff --git a/conftest.py b/conftest.py index fe0afdf1..f0905e04 100644 --- a/conftest.py +++ b/conftest.py @@ -1,22 +1,25 @@ +import importlib from django.conf import settings import django import pytest -from django.core.management import call_command from RIGS.models import VatRate -from PyRIGS.tests import pages import os -from selenium import webdriver + +# z3c.rml got ``del importlib.metadata`` in __init__.py +# reinject the original one here to solve ``module 'importlib' has no attribute 'metadata'`` +# what a bullsh*t +import importlib.metadata as _importlib_metadata + +importlib.metadata = _importlib_metadata def pytest_configure(): - settings.PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.MD5PasswordHasher', - ) + settings.PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",) settings.WHITENOISE_USE_FINDERS = True settings.WHITENOISE_AUTOREFRESH = True # TODO Why do we need this, with the above options enabled? settings.STATICFILES_DIRS += [ - os.path.join(settings.BASE_DIR, 'static/'), + os.path.join(settings.BASE_DIR, "static/"), ] django.setup() @@ -33,36 +36,9 @@ def admin_user(admin_user): return admin_user -@pytest.fixture -def logged_in_browser(live_server, admin_user, browser, db): - login_page = pages.LoginPage(browser.driver, live_server.url).open() - login_page.login(admin_user.username, "password") - yield browser - - -@pytest.fixture(scope='session') -def splinter_driver_kwargs(): - options = webdriver.ChromeOptions() - options.add_argument("--window-size=1920,1080") - options.add_argument("--headless") - if settings.CI: - options.add_argument("--no-sandbox") - return {"options": options} - - -@pytest.fixture(scope='session') -def splinter_webdriver(): - return 'chrome' - - -@pytest.fixture(scope='session') -def splinter_screenshot_dir(): - return 'screenshots/' - - @pytest.fixture(autouse=True) # Also enables DB access for all tests as a useful side effect def vat_rate(db): - vat_rate = VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + vat_rate = VatRate.objects.create(start_at="2014-03-05", rate=0.20, comment="test1") yield vat_rate vat_rate.delete() diff --git a/gulpfile.js b/gulpfile.js index b1a1d9ff..3233846b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -3,7 +3,7 @@ var gulp = require('gulp'); const terser = require('gulp-uglify'); -const sass = require('gulp-sass')(require('node-sass')); +const sass = require('gulp-sass')(require('sass')); const flatten = require('gulp-flatten'); const autoprefixer = require('autoprefixer') const postcss = require('gulp-postcss') @@ -30,7 +30,14 @@ function styles(done) { 'node_modules/easymde/dist/easymde.min.css' ]) .pipe(sourcemaps.init()) - .pipe(sass().on('error', sass.logError)) + // Bootstrap 4's SCSS can only be consumed via @import (its partials share global variables), + // so that one deprecation stays silenced until we move to a Sass-module-aware framework. + // quietDeps hides the remaining deprecations raised inside third-party stylesheets. + .pipe(sass({ + loadPaths: ['.'], + quietDeps: true, + silenceDeprecations: ['import'], + }).on('error', sass.logError)) .pipe(gulpif(function(file) { return bs_select.includes(file.relative);}, con('selects.css'))) .pipe(postcss([ autoprefixer(), cssnano() ])) .pipe(sourcemaps.write()) @@ -71,7 +78,7 @@ function scripts() { .pipe(gulpif(function(file) { return jpop.includes(file.relative);}, con('jpop.js'))) .pipe(flatten()) // Only minify if filename does not already denote it as minified - .pipe(gulpif(function(file) { return file.path.indexOf("min") == -1;},terser())) + .pipe(gulpif(function(file) { return file.path.indexOf("min") === -1;},terser())) .pipe(gulp.dest(dest)) .pipe(browsersync.stream()); } diff --git a/nginx/default.conf.example b/nginx/default.conf.example new file mode 100644 index 00000000..7eee3fdf --- /dev/null +++ b/nginx/default.conf.example @@ -0,0 +1,46 @@ +upstream pyrigs { + server pyrigs:8000; +} + +server { + listen 80; + server_name _; + + # Let's Encrypt HTTP-01 + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + http2 on; + server_name _; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + + client_max_body_size 20m; + + # Serve collected static files directly rather than via WhiteNoise + location /static/ { + alias /var/www/static/; + access_log off; + expires 30d; + add_header Cache-Control "public, immutable"; + } + + location / { + proxy_pass http://pyrigs; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_redirect off; + } +} diff --git a/nginx/default.dev.conf b/nginx/default.dev.conf new file mode 100644 index 00000000..8455d2d7 --- /dev/null +++ b/nginx/default.dev.conf @@ -0,0 +1,25 @@ +# Local development: plain HTTP, no certificates. Use default.conf.example for production. +upstream pyrigs { + server pyrigs:8000; +} + +server { + listen 80; + server_name _; + + client_max_body_size 20m; + + location /static/ { + alias /var/www/static/; + access_log off; + } + + location / { + proxy_pass http://pyrigs; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + } +} diff --git a/package-lock.json b/package-lock.json index d42c3d98..538ee157 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,117 +18,30 @@ "bootstrap-select": "^1.13.18", "clipboard": "^2.0.8", "cssnano": "^5.0.13", - "easymde": "^2.16.1", + "easymde": "^2.21.0", "fullcalendar": "^5.10.1", "gulp-concat": "^2.6.1", "gulp-flatten": "^0.4.0", "gulp-if": "^3.0.0", "gulp-postcss": "^9.0.1", - "gulp-sass": "^5.0.0", + "gulp-sass": "^6.0.1", "gulp-sourcemaps": "^3.0.0", "gulp-uglify": "^3.0.2", "html5sortable": "^0.13.3", "jquery": "^3.6.0", - "konami": "^1.6.3", + "konami": "^1.7.0", "moment": "^2.29.4", - "node-sass": "^9.0.0", "popper.js": "^1.16.1", - "postcss": "^8.4.31", + "postcss": "^8.5.26", + "sass": "^1.103.1", "uglify-js": "^3.14.5" }, "devDependencies": { "browser-sync": "^3.0.2", "gulp": "^5.0.0" - } - }, - "node_modules/@babel/code-frame": { - "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.24.7", - "picocolors": "^1.0.0" }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "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/highlight": { - "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.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" + "node": "24.x" } }, "node_modules/@forevolve/bootstrap-dark": { @@ -150,11 +63,6 @@ "node": ">=6" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" - }, "node_modules/@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", @@ -170,27 +78,6 @@ "node": ">= 0.10" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, "node_modules/@gulp-sourcemaps/identity-map/node_modules/through2": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", @@ -244,29 +131,292 @@ "node": ">=10.13.0" } }, - "node_modules/@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, "dependencies": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" } }, - "node_modules/@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@socket.io/component-emitter": { @@ -275,22 +425,6 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/codemirror": { "version": "5.60.15", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", @@ -318,11 +452,6 @@ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==" }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" - }, "node_modules/@types/node": { "version": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", @@ -332,11 +461,6 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" - }, "node_modules/@types/tern": { "version": "0.23.9", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", @@ -345,10 +469,15 @@ "@types/estree": "*" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, "node_modules/accepts": { "version": "1.3.8", @@ -374,61 +503,6 @@ "node": ">=0.4.0" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajax-bootstrap-select": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/ajax-bootstrap-select/-/ajax-bootstrap-select-1.4.5.tgz", @@ -493,6 +567,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -524,24 +599,6 @@ "node": ">= 8" } }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/arr-diff": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", @@ -595,14 +652,6 @@ "node": ">=0.10.0" } }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -643,14 +692,6 @@ "node": ">=0.8.0" } }, - "node_modules/async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==", - "engines": { - "node": "*" - } - }, "node_modules/async-settle": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", @@ -776,6 +817,18 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -839,9 +892,10 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -931,9 +985,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "funding": [ { "type": "opencollective", @@ -948,11 +1002,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -1000,96 +1056,6 @@ "node": ">= 0.8" } }, - "node_modules/cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "dependencies": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -1102,9 +1068,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001639", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", - "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "funding": [ { "type": "opencollective", @@ -1118,12 +1084,14 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1159,22 +1127,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "engines": { - "node": ">=6" - } - }, "node_modules/clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -1189,6 +1141,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1273,6 +1226,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1283,7 +1237,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", @@ -1294,9 +1249,10 @@ } }, "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", + "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", @@ -1341,11 +1297,6 @@ "node": ">=0.8" } }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -1391,19 +1342,6 @@ "node": ">= 0.10" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/css": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", @@ -1602,37 +1540,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -1646,11 +1553,6 @@ "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1679,6 +1581,16 @@ "node": ">=0.10.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", @@ -1787,21 +1699,22 @@ } }, "node_modules/easymde": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.18.0.tgz", - "integrity": "sha512-IxVVUxNWIoXLeqtBU4BLc+eS/ScYhT1Dcb6yF5Wchoj1iXAV+TIIDWx+NCaZhY7RcSHqDPKllbYq7nwGKILnoA==", + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.21.0.tgz", + "integrity": "sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==", + "license": "MIT", "dependencies": { - "@types/codemirror": "^5.60.4", + "@types/codemirror": "^5.60.10", "@types/marked": "^4.0.7", - "codemirror": "^5.63.1", + "codemirror": "^5.65.15", "codemirror-spell-checker": "1.1.2", "marked": "^4.1.0" } }, "node_modules/eazy-logger": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", - "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.1.0.tgz", + "integrity": "sha512-+mn7lRm+Zf1UT/YaH8WXtpU6PIV2iOjzP6jgKoiaq/VNrjYKp+OHZGe2znaLgDeFkw8cL9ffuaUm+nNnzcYyGw==", "dev": true, "dependencies": { "chalk": "4.1.2" @@ -1817,14 +1730,16 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.815", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", - "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==" + "version": "1.5.443", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.443.tgz", + "integrity": "sha512-TDJG36L9A3CWWwZ97HKaE+Iz1sW80pNp6sAU0owzCIV3Zc9eVyRqZK8Vz6NKuMIU8WjP0pIkoqCa0VRf/yFD0Q==", + "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/encodeurl": { "version": "1.0.2", @@ -1835,15 +1750,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1853,43 +1759,46 @@ } }, "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "version": "6.6.11", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.11.tgz", + "integrity": "sha512-z34mGpLoY/82ZbkX1LqGMIaf3wjVMPL6gVC681QDqnmzJ26synI8qAE32P+a7UazmpJZ8UGrFIeAavgRfXzYrg==", "dev": true, + "license": "MIT", "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", "accepts": "~1.3.4", - "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.21.0" }, "engines": { "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", "dev": true, + "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", + "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -1906,7 +1815,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/engine.io-parser": { "version": "5.2.3", @@ -1918,10 +1828,11 @@ } }, "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -1938,7 +1849,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/entities": { "version": "2.2.0", @@ -1948,27 +1860,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es5-ext": { "version": "0.10.64", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", @@ -2018,9 +1909,10 @@ } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -2031,14 +1923,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", @@ -2192,18 +2076,6 @@ "node": ">= 0.8" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/findup-sync": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", @@ -2245,9 +2117,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -2255,6 +2127,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -2322,17 +2195,6 @@ "universalify": "^0.1.0" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/fs-mkdirp-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", @@ -2346,11 +2208,6 @@ "node": ">=10.13.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2374,76 +2231,20 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dependencies": { - "globule": "^1.0.0" - }, - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -2542,50 +2343,6 @@ "which": "bin/which" } }, - "node_modules/globule": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", - "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", - "dependencies": { - "glob": "~7.1.1", - "lodash": "^4.17.21", - "minimatch": "~3.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/globule/node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globule/node_modules/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/glogg": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", @@ -2784,9 +2541,10 @@ } }, "node_modules/gulp-sass": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-5.1.0.tgz", - "integrity": "sha512-7VT0uaF+VZCmkNBglfe1b34bxn/AfcssquLKVDYnCDJ3xNBaW7cUuI3p3BQmoKcoKFrs9jdzUxyb+u+NGfL4OQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-6.0.1.tgz", + "integrity": "sha512-4wonidxB8lGPHvahelpGavUBJAuERSl+OIVxPCyQthK4lSJhZ/u3/qjFcyAtnMIXDl6fXTn34H4BXsN7gt54kQ==", + "license": "MIT", "dependencies": { "lodash.clonedeep": "^4.5.0", "picocolors": "^1.0.0", @@ -2909,18 +2667,11 @@ "node": ">= 10.13.0" } }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "engines": { - "node": ">=6" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -2944,15 +2695,11 @@ "node": ">= 0.10" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -2972,38 +2719,11 @@ "node": ">=0.10.0" } }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/html5sortable": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/html5sortable/-/html5sortable-0.13.3.tgz", "integrity": "sha512-rmglSaNttGo4LY33PFW51mgeD1ItvHyfS9cRCD+Cj9Msj/xFaG/sZjLGVtPbtxYJmhY/c8jtw6G07yWhC2ifEw==" }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3043,86 +2763,11 @@ "node": ">=8.0.0" } }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, + "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -3151,44 +2796,11 @@ ] }, "node_modules/immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", + "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } + "license": "MIT" }, "node_modules/inherits": { "version": "2.0.4", @@ -3210,18 +2822,6 @@ "node": ">=10.13.0" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -3235,11 +2835,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3256,6 +2851,7 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dev": true, "dependencies": { "hasown": "^2.0.2" }, @@ -3292,7 +2888,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -3301,6 +2897,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -3309,7 +2906,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3317,11 +2914,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" - }, "node_modules/is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", @@ -3349,14 +2941,6 @@ "lodash.isfinite": "^3.3.2" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -3430,7 +3014,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/isobject": { "version": "3.0.1", @@ -3445,26 +3030,6 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, - "node_modules/js-base64": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, "node_modules/jsonfile": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", @@ -3474,18 +3039,11 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/konami": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/konami/-/konami-1.6.3.tgz", - "integrity": "sha512-QRr/cWcS4Mm9XswKyLXT9w/ygf9f6gTdFw/gExd0komDbFOdgME/M0pjD59hgh2uNJWpDT/+89l80/xQiK5niA==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/konami/-/konami-1.7.0.tgz", + "integrity": "sha512-lfw/bXyPcOYVP0Vt05PKVYWp/Rx92eUIg48Kb2jkY2Ko00jWMTzk6jt5bRdkVRfvPr9RLTl1DM6vVrjWLXnQyg==", + "license": "MIT" }, "node_modules/last-run": { "version": "2.0.0", @@ -3537,26 +3095,12 @@ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", "dev": true }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.clonedeep": { "version": "4.5.0", @@ -3579,14 +3123,6 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, - "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "engines": { - "node": ">=12" - } - }, "node_modules/lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -3608,32 +3144,6 @@ "make-error": "^1.2.0" } }, - "node_modules/make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -3643,17 +3153,6 @@ "node": ">=0.10.0" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -3688,31 +3187,6 @@ "node": ">=0.12" } }, - "node_modules/meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3764,18 +3238,11 @@ "node": ">= 0.6" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3783,123 +3250,17 @@ "node": "*" } }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", - "dependencies": { - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/mitt": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", "dev": true }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", + "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==", + "license": "MIT", "engines": { "node": "*" } @@ -3907,7 +3268,8 @@ "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/mute-stdout": { "version": "2.0.0", @@ -3918,21 +3280,17 @@ "node": ">= 10.13.0" } }, - "node_modules/nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" - }, "node_modules/nanoid": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz", - "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -3944,6 +3302,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -3953,273 +3312,20 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">= 10.12.0" - } - }, - "node_modules/node-gyp/node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/node-gyp/node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/node-gyp/node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/node-gyp/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/node-gyp/node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/node-gyp/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/node-gyp/node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/node-gyp/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/node-gyp/node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/node-gyp/node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/node-gyp/node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/node-gyp/node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dependencies": { - "imurmurhash": "^0.1.4" - } + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" - }, - "node_modules/node-sass": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-9.0.0.tgz", - "integrity": "sha512-yltEuuLrfH6M7Pq2gAj5B6Zm7m+gdZoG66wTqG6mIZV/zijq3M2OO2HswtT6oBspPyFhHDcaxWpsBm0fRNDHPg==", - "hasInstallScript": true, - "dependencies": { - "async-foreach": "^0.1.3", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "lodash": "^4.17.15", - "make-fetch-happen": "^10.0.4", - "meow": "^9.0.0", - "nan": "^2.17.0", - "node-gyp": "^8.4.1", - "sass-graph": "^4.0.1", - "stdout-stream": "^1.4.0", - "true-case-path": "^2.2.1" - }, - "bin": { - "node-sass": "bin/node-sass" - }, + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "license": "MIT", "engines": { - "node": ">=16" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/normalize-path": { @@ -4261,21 +3367,6 @@ "node": ">= 10.13.0" } }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -4354,53 +3445,6 @@ "node": ">=4" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -4415,23 +3459,6 @@ "node": ">=0.8" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -4458,34 +3485,11 @@ "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-root": { "version": "0.1.1", @@ -4509,15 +3513,17 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -4584,9 +3590,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -4601,10 +3607,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4988,9 +3995,10 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -5038,37 +4046,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/queue-tick": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", "dev": true }, - "node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "engines": { - "node": ">=8" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5105,76 +4088,6 @@ "node": ">=0.10.0" } }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "engines": { - "node": ">=8" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5212,18 +4125,6 @@ "node": ">= 10.13.0" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -5250,6 +4151,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5264,6 +4166,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -5314,14 +4217,6 @@ "node": ">= 0.8.0" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5332,21 +4227,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rx": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", @@ -5376,40 +4256,69 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true + "dev": true }, - "node_modules/sass-graph": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz", - "integrity": "sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==", + "node_modules/sass": { + "version": "1.103.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.103.1.tgz", + "integrity": "sha512-9icZURbP51S6S0QGoyaeqk9uB06GNWxsFYWfH5RgpFgqK5FA8tJcM3AdVxrZEVJ7dz+L87nG95gBKf4VuaMHGw==", + "license": "MIT", "dependencies": { - "glob": "^7.0.0", - "lodash": "^4.17.11", - "scss-tokenizer": "^0.4.3", - "yargs": "^17.2.1" + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { - "sassgraph": "bin/sassgraph" + "sass": "sass.js" }, "engines": { - "node": ">=12" + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, - "node_modules/scss-tokenizer": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz", - "integrity": "sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==", + "node_modules/sass/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", "dependencies": { - "js-base64": "^2.4.9", - "source-map": "^0.7.3" + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/scss-tokenizer/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "node_modules/sass/node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "license": "MIT" + }, + "node_modules/sass/node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" } }, "node_modules/select": { @@ -5417,17 +4326,6 @@ "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" }, - "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/semver-greatest-satisfied-range": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", @@ -5653,50 +4551,12 @@ "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", "dev": true }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -5716,22 +4576,24 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", "dev": true, + "license": "MIT", "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" + "debug": "~4.4.1", + "ws": "~8.21.0" } }, "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -5743,10 +4605,11 @@ } }, "node_modules/socket.io-adapter/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/socket.io-client": { "version": "4.8.1", @@ -5787,25 +4650,27 @@ "dev": true }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", "dev": true, + "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -5817,10 +4682,11 @@ } }, "node_modules/socket.io-parser/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/socket.io/node_modules/debug": { "version": "4.3.5", @@ -5845,53 +4711,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socks-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5901,9 +4720,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5927,50 +4747,6 @@ "node": ">= 10.13.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==" - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, - "node_modules/ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -5986,41 +4762,6 @@ "node": ">= 0.6" } }, - "node_modules/stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "dependencies": { - "readable-stream": "^2.0.1" - } - }, - "node_modules/stdout-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/stdout-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/stdout-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/stream-composer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", @@ -6083,6 +4824,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6111,17 +4853,6 @@ "node": ">=0.10.0" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -6141,6 +4872,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -6152,6 +4884,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -6179,16 +4912,17 @@ } }, "node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.4.tgz", + "integrity": "sha512-2GJ4h3rl13qYTdwllaK6QlL8tG+UrM8626V2Ylcd/yBUv2Y/EwLsYisVV6UDCRmlk+C74G8nMpxJCk0RWPdDCw==", + "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", + "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": { @@ -6206,30 +4940,6 @@ "node": ">= 10" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -6362,35 +5072,11 @@ "node": ">=0.6" } }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/true-case-path": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-2.2.1.tgz", - "integrity": "sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q==" - }, "node_modules/type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" }, - "node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typo-js": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz", @@ -6469,28 +5155,6 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true }, - "node_modules/unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "dependencies": { - "unique-slug": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -6510,9 +5174,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "funding": [ { "type": "opencollective", @@ -6527,9 +5191,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -6561,15 +5226,6 @@ "node": ">= 10.13.0" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/value-or-function": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", @@ -6737,32 +5393,11 @@ "node": ">= 0.10" } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -6781,10 +5416,11 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -6822,19 +5458,16 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "engines": { "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", "engines": { "node": ">= 6" } @@ -6843,6 +5476,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -6860,6 +5494,7 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, "engines": { "node": ">=10" } @@ -6868,83 +5503,13 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "engines": { "node": ">=12" } } }, "dependencies": { - "@babel/code-frame": { - "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.24.7", - "picocolors": "^1.0.0" - } - }, - "@babel/helper-validator-identifier": { - "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/highlight": { - "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.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "@forevolve/bootstrap-dark": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@forevolve/bootstrap-dark/-/bootstrap-dark-2.1.1.tgz", @@ -6960,11 +5525,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==" }, - "@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" - }, "@gulp-sourcemaps/identity-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", @@ -6972,25 +5532,11 @@ "requires": { "acorn": "^6.4.1", "normalize-path": "^3.0.0", - "postcss": "^7.0.16", + "postcss": "^8.5.26", "source-map": "^0.6.0", "through2": "^3.0.1" }, "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==" - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, "through2": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", @@ -7036,23 +5582,109 @@ "is-negated-glob": "^1.0.0" } }, - "@npmcli/fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", - "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "optional": true, "requires": { - "@gar/promisify": "^1.1.3", - "semver": "^7.3.5" + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0", + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "dependencies": { + "picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "optional": true + } } }, - "@npmcli/move-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", - "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - } + "@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "optional": true + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "optional": true + }, + "@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "optional": true + }, + "@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "optional": true + }, + "@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "optional": true + }, + "@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "optional": true + }, + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "optional": true + }, + "@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "optional": true + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "optional": true + }, + "@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "optional": true + }, + "@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "optional": true + }, + "@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "optional": true }, "@socket.io/component-emitter": { "version": "3.1.2", @@ -7060,16 +5692,6 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true }, - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" - }, - "@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" - }, "@types/codemirror": { "version": "5.60.15", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", @@ -7097,11 +5719,6 @@ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==" }, - "@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" - }, "@types/node": { "version": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", @@ -7111,11 +5728,6 @@ "undici-types": "~6.20.0" } }, - "@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==" - }, "@types/tern": { "version": "0.23.9", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", @@ -7124,10 +5736,14 @@ "@types/estree": "*" } }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "accepts": { "version": "1.3.8", @@ -7144,46 +5760,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==" }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", - "requires": { - "humanize-ms": "^1.2.1" - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, "ajax-bootstrap-select": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/ajax-bootstrap-select/-/ajax-bootstrap-select-1.4.5.tgz", @@ -7230,6 +5806,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -7249,20 +5826,6 @@ "picomatch": "^2.0.4" } }, - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, "arr-diff": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", @@ -7300,11 +5863,6 @@ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==" - }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -7336,11 +5894,6 @@ "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", "dev": true }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==" - }, "async-settle": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", @@ -7414,6 +5967,11 @@ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true }, + "baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -7455,9 +6013,9 @@ "requires": {} }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7492,7 +6050,7 @@ "fresh": "^0.5.2", "fs-extra": "3.0.1", "http-proxy": "^1.18.1", - "immutable": "^3", + "immutable": "^4.3.9", "micromatch": "^4.0.8", "opn": "5.3.0", "portscanner": "2.2.0", @@ -7528,21 +6086,22 @@ "async-each-series": "0.1.1", "chalk": "4.1.2", "connect-history-api-fallback": "^1", - "immutable": "^3", + "immutable": "^4.3.9", "server-destroy": "1.0.1", "socket.io-client": "^4.4.1", "stream-throttle": "^0.1.3" } }, "browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "requires": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" } }, "bs-recipes": { @@ -7567,76 +6126,6 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true }, - "cacache": { - "version": "16.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", - "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", - "requires": { - "@npmcli/fs": "^2.1.0", - "@npmcli/move-file": "^2.0.0", - "chownr": "^2.0.0", - "fs-minipass": "^2.1.0", - "glob": "^8.0.1", - "infer-owner": "^1.0.4", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "mkdirp": "^1.0.4", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^9.0.0", - "tar": "^6.1.11", - "unique-filename": "^2.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, "caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -7649,14 +6138,15 @@ } }, "caniuse-lite": { - "version": "1.0.30001639", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz", - "integrity": "sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==" + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==" }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7678,16 +6168,6 @@ "readdirp": "~3.6.0" } }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" - }, "clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -7702,6 +6182,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -7779,6 +6260,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -7786,7 +6268,8 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "color-support": { "version": "1.1.3", @@ -7794,9 +6277,9 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, "colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==" }, "commander": { "version": "2.20.3", @@ -7835,11 +6318,6 @@ "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, "convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -7876,16 +6354,6 @@ "vary": "^1" } }, - "cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, "css": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", @@ -8036,27 +6504,6 @@ } } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" - }, - "decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==" - } - } - }, "decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -8067,11 +6514,6 @@ "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8090,6 +6532,12 @@ "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", "dev": true }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "optional": true + }, "detect-newline": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", @@ -8165,21 +6613,21 @@ } }, "easymde": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.18.0.tgz", - "integrity": "sha512-IxVVUxNWIoXLeqtBU4BLc+eS/ScYhT1Dcb6yF5Wchoj1iXAV+TIIDWx+NCaZhY7RcSHqDPKllbYq7nwGKILnoA==", + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.21.0.tgz", + "integrity": "sha512-5uE7I/DEN8gvGRwxaqAv7h1PMEK2ykNXVX5zL0dK3nCYROGja3AMbdQz8eCEELnfvCfy7tRkTmLuvyJG8uSWjQ==", "requires": { - "@types/codemirror": "^5.60.4", + "@types/codemirror": "^5.60.10", "@types/marked": "^4.0.7", - "codemirror": "^5.63.1", + "codemirror": "^5.65.15", "codemirror-spell-checker": "1.1.2", "marked": "^4.1.0" } }, "eazy-logger": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", - "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.1.0.tgz", + "integrity": "sha512-+mn7lRm+Zf1UT/YaH8WXtpU6PIV2iOjzP6jgKoiaq/VNrjYKp+OHZGe2znaLgDeFkw8cL9ffuaUm+nNnzcYyGw==", "dev": true, "requires": { "chalk": "4.1.2" @@ -8192,14 +6640,15 @@ "dev": true }, "electron-to-chromium": { - "version": "1.4.815", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", - "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==" + "version": "1.5.443", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.443.tgz", + "integrity": "sha512-TDJG36L9A3CWWwZ97HKaE+Iz1sW80pNp6sAU0owzCIV3Zc9eVyRqZK8Vz6NKuMIU8WjP0pIkoqCa0VRf/yFD0Q==" }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "encodeurl": { "version": "1.0.2", @@ -8207,15 +6656,6 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "requires": { - "iconv-lite": "^0.6.2" - } - }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -8225,26 +6665,26 @@ } }, "engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "version": "6.6.11", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.11.tgz", + "integrity": "sha512-z34mGpLoY/82ZbkX1LqGMIaf3wjVMPL6gVC681QDqnmzJ26synI8qAE32P+a7UazmpJZ8UGrFIeAavgRfXzYrg==", "dev": true, "requires": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", "accepts": "~1.3.4", - "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.21.0" }, "dependencies": { "debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "requires": { "ms": "^2.1.3" @@ -8259,22 +6699,22 @@ } }, "engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", "dev": true, "requires": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", + "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" }, "dependencies": { "debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "requires": { "ms": "^2.1.3" @@ -8299,24 +6739,6 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" - }, - "err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, "es5-ext": { "version": "0.10.64", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", @@ -8359,9 +6781,9 @@ } }, "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, "escape-html": { "version": "1.0.3", @@ -8369,11 +6791,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" - }, "esniff": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", @@ -8503,15 +6920,6 @@ "unpipe": "~1.0.0" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, "findup-sync": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", @@ -8544,9 +6952,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true }, "for-in": { @@ -8591,14 +6999,6 @@ "universalify": "^0.1.0" } }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, "fs-mkdirp-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", @@ -8609,11 +7009,6 @@ "streamx": "^2.12.0" } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8629,53 +7024,14 @@ "function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "requires": { - "globule": "^1.0.0" - } + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==" - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true }, "glob-parent": { "version": "5.1.2", @@ -8758,39 +7114,6 @@ } } }, - "globule": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", - "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", - "requires": { - "glob": "~7.1.1", - "lodash": "^4.17.21", - "minimatch": "~3.0.2" - }, - "dependencies": { - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, "glogg": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", @@ -8956,9 +7279,9 @@ } }, "gulp-sass": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-5.1.0.tgz", - "integrity": "sha512-7VT0uaF+VZCmkNBglfe1b34bxn/AfcssquLKVDYnCDJ3xNBaW7cUuI3p3BQmoKcoKFrs9jdzUxyb+u+NGfL4OQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/gulp-sass/-/gulp-sass-6.0.1.tgz", + "integrity": "sha512-4wonidxB8lGPHvahelpGavUBJAuERSl+OIVxPCyQthK4lSJhZ/u3/qjFcyAtnMIXDl6fXTn34H4BXsN7gt54kQ==", "requires": { "lodash.clonedeep": "^4.5.0", "picocolors": "^1.0.0", @@ -9058,15 +7381,11 @@ "glogg": "^2.2.0" } }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==" - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "has-gulplog": { "version": "0.1.0", @@ -9083,15 +7402,11 @@ } } }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, "hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "requires": { "function-bind": "^1.1.2" } @@ -9105,34 +7420,11 @@ "parse-passwd": "^1.0.0" } }, - "hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - } - } - }, "html5sortable": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/html5sortable/-/html5sortable-0.13.3.tgz", "integrity": "sha512-rmglSaNttGo4LY33PFW51mgeD1ItvHyfS9cRCD+Cj9Msj/xFaG/sZjLGVtPbtxYJmhY/c8jtw6G07yWhC2ifEw==" }, - "http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -9165,68 +7457,11 @@ "requires-port": "^1.0.0" } }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "requires": { - "agent-base": "6", - "debug": "4" - }, - "dependencies": { - "debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "requires": { - "ms": "^2.0.0" - } - }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "devOptional": true, + "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -9238,35 +7473,11 @@ "dev": true }, "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", + "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", "dev": true }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -9284,15 +7495,6 @@ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true }, - "ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", - "requires": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - } - }, "is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -9303,11 +7505,6 @@ "is-windows": "^1.0.1" } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -9321,6 +7518,7 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dev": true, "requires": { "hasown": "^2.0.2" } @@ -9347,27 +7545,23 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true + "devOptional": true }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "requires": { "is-extglob": "^2.1.1" } }, - "is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" - }, "is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", @@ -9389,11 +7583,6 @@ "lodash.isfinite": "^3.3.2" } }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" - }, "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -9449,7 +7638,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "isobject": { "version": "3.0.1", @@ -9461,26 +7651,6 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, - "js-base64": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", - "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, "jsonfile": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", @@ -9490,15 +7660,10 @@ "graceful-fs": "^4.1.6" } }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - }, "konami": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/konami/-/konami-1.6.3.tgz", - "integrity": "sha512-QRr/cWcS4Mm9XswKyLXT9w/ygf9f6gTdFw/gExd0komDbFOdgME/M0pjD59hgh2uNJWpDT/+89l80/xQiK5niA==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/konami/-/konami-1.7.0.tgz", + "integrity": "sha512-lfw/bXyPcOYVP0Vt05PKVYWp/Rx92eUIg48Kb2jkY2Ko00jWMTzk6jt5bRdkVRfvPr9RLTl1DM6vVrjWLXnQyg==" }, "last-run": { "version": "2.0.0", @@ -9538,23 +7703,11 @@ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", "dev": true }, - "lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true }, "lodash.clonedeep": { "version": "4.5.0", @@ -9577,11 +7730,6 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" - }, "lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -9603,40 +7751,12 @@ "make-error": "^1.2.0" } }, - "make-fetch-happen": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", - "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", - "requires": { - "agentkeepalive": "^4.2.1", - "cacache": "^16.1.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^3.1.6", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^2.0.3", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^9.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true }, - "map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==" - }, "marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -9662,25 +7782,6 @@ "timers-ext": "^0.1.7" } }, - "meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9717,109 +7818,30 @@ "mime-db": "1.52.0" } }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" - }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "requires": { "brace-expansion": "^1.1.7" } }, - "minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - } - }, - "minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "requires": { - "yallist": "^4.0.0" - } - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-fetch": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", - "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", - "requires": { - "encoding": "^0.1.13", - "minipass": "^3.1.6", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "requires": { - "minipass": "^3.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, "mitt": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", "dev": true }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, "moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", + "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==" }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "mute-stdout": { "version": "2.0.0", @@ -9827,236 +7849,32 @@ "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", "dev": true }, - "nan": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", - "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" - }, "nanoid": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz", - "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==" + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==" }, "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true }, "next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, - "node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "requires": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "dependencies": { - "@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "requires": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - } - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==" - }, - "cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "requires": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - } - }, - "debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "requires": { - "ms": "2.1.2" - } - }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "requires": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - } - }, - "minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "requires": { - "encoding": "^0.1.12", - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - } - }, - "ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "requires": { - "minipass": "^3.1.1" - } - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "requires": { - "imurmurhash": "^0.1.4" - } - } - } + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "optional": true }, "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" - }, - "node-sass": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-9.0.0.tgz", - "integrity": "sha512-yltEuuLrfH6M7Pq2gAj5B6Zm7m+gdZoG66wTqG6mIZV/zijq3M2OO2HswtT6oBspPyFhHDcaxWpsBm0fRNDHPg==", - "requires": { - "async-foreach": "^0.1.3", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", - "glob": "^7.0.3", - "lodash": "^4.17.15", - "make-fetch-happen": "^10.0.4", - "meow": "^9.0.0", - "nan": "^2.17.0", - "node-gyp": "^8.4.1", - "sass-graph": "^4.0.1", - "stdout-stream": "^1.4.0", - "true-case-path": "^2.2.1" - } - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==" }, "normalize-path": { "version": "3.0.0", @@ -10082,17 +7900,6 @@ "once": "^1.4.0" } }, - "npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "requires": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - } - }, "nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -10153,35 +7960,6 @@ "is-wsl": "^1.1.0" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, "parse-filepath": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", @@ -10193,17 +7971,6 @@ "path-root": "^0.1.1" } }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, "parse-node-version": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", @@ -10221,25 +7988,11 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "path-root": { "version": "0.1.1", @@ -10257,14 +8010,14 @@ "dev": true }, "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "plugin-error": { @@ -10310,13 +8063,13 @@ } }, "postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "requires": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" } }, "postcss-calc": { @@ -10536,9 +8289,9 @@ } }, "postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -10571,31 +8324,12 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" - }, - "promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "requires": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - } - }, "queue-tick": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", "dev": true }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==" - }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -10625,62 +8359,6 @@ } } }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" - } - } - }, "readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -10709,15 +8387,6 @@ "resolve": "^1.20.0" } }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -10737,7 +8406,8 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true }, "requires-port": { "version": "1.0.0", @@ -10749,6 +8419,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "requires": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -10784,25 +8455,12 @@ "minimatch": "^3.0.2" } }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==" - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, "rx": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", @@ -10818,45 +8476,48 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true + "dev": true }, - "sass-graph": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz", - "integrity": "sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==", + "sass": { + "version": "1.103.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.103.1.tgz", + "integrity": "sha512-9icZURbP51S6S0QGoyaeqk9uB06GNWxsFYWfH5RgpFgqK5FA8tJcM3AdVxrZEVJ7dz+L87nG95gBKf4VuaMHGw==", "requires": { - "glob": "^7.0.0", - "lodash": "^4.17.11", - "scss-tokenizer": "^0.4.3", - "yargs": "^17.2.1" - } - }, - "scss-tokenizer": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz", - "integrity": "sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==", - "requires": { - "js-base64": "^2.4.9", - "source-map": "^0.7.3" + "@parcel/watcher": "^2.4.1", + "chokidar": "^5.0.0", + "immutable": "^4.3.9", + "source-map-js": ">=0.6.2 <2.0.0" }, "dependencies": { - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" + "chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "requires": { + "readdirp": "^5.0.0" + } + }, + "immutable": { + "version": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==" + }, + "readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==" } } }, + "sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==" + }, "select": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" }, - "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" - }, "semver-greatest-satisfied-range": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", @@ -11045,40 +8706,12 @@ "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", "dev": true }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" - }, "socket.io": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", @@ -11112,28 +8745,28 @@ } }, "socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", "dev": true, "requires": { - "debug": "~4.3.4", - "ws": "~8.17.1" + "debug": "~4.4.1", + "ws": "~8.21.0" }, "dependencies": { "debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true } } @@ -11168,75 +8801,41 @@ } }, "socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", "dev": true, "requires": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "dependencies": { "debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true } } }, - "socks": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", - "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", - "requires": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - } - }, - "socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "dependencies": { - "debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, "source-map-resolve": { "version": "0.6.0", @@ -11253,47 +8852,6 @@ "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", "dev": true }, - "spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==" - }, - "sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" - }, - "ssri": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", - "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", - "requires": { - "minipass": "^3.1.1" - } - }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -11305,43 +8863,6 @@ "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", "dev": true }, - "stdout-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", - "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", - "requires": { - "readable-stream": "^2.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "stream-composer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", @@ -11396,6 +8917,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -11415,14 +8937,6 @@ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==" }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "requires": { - "min-indent": "^1.0.0" - } - }, "stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -11436,6 +8950,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -11443,7 +8958,8 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true }, "sver": { "version": "1.8.4", @@ -11464,16 +8980,16 @@ } }, "svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.4.tgz", + "integrity": "sha512-2GJ4h3rl13qYTdwllaK6QlL8tG+UrM8626V2Ylcd/yBUv2Y/EwLsYisVV6UDCRmlk+C74G8nMpxJCk0RWPdDCw==", "requires": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", + "sax": "^1.5.0", "stable": "^0.1.8" }, "dependencies": { @@ -11484,26 +9000,6 @@ } } }, - "tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "dependencies": { - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" - } - } - }, "teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -11625,26 +9121,11 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==" - }, - "true-case-path": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-2.2.1.tgz", - "integrity": "sha512-0z3j8R7MCjy10kc/g+qg7Ln3alJTodw9aDuVWZa3uiWqfuBMKeAeP2ocWcxoyM3D73yz3Jt/Pu4qPr4wHSdB/Q==" - }, "type": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==" - }, "typo-js": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz", @@ -11691,22 +9172,6 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true }, - "unique-filename": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", - "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", - "requires": { - "unique-slug": "^3.0.0" - } - }, - "unique-slug": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", - "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", - "requires": { - "imurmurhash": "^0.1.4" - } - }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -11720,12 +9185,12 @@ "dev": true }, "update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "requires": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" } }, "util-deprecate": { @@ -11745,15 +9210,6 @@ "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "value-or-function": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", @@ -11898,26 +9354,11 @@ } } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" - } - }, - "wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "requires": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -11930,9 +9371,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "requires": {} }, @@ -11950,22 +9391,19 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==" }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "requires": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -11979,14 +9417,16 @@ "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true } } }, "yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true } } } diff --git a/package.json b/package.json index 66409d64..77ea718c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Tom Price", "license": "Custom", "engines": { - "node": "22.x" + "node": "24.x" }, "dependencies": { "@forevolve/bootstrap-dark": "^2.1.0", @@ -17,22 +17,22 @@ "bootstrap-select": "^1.13.18", "clipboard": "^2.0.8", "cssnano": "^5.0.13", - "easymde": "^2.16.1", + "easymde": "^2.21.0", "fullcalendar": "^5.10.1", "gulp-concat": "^2.6.1", "gulp-flatten": "^0.4.0", "gulp-if": "^3.0.0", "gulp-postcss": "^9.0.1", - "gulp-sass": "^5.0.0", + "gulp-sass": "^6.0.1", "gulp-sourcemaps": "^3.0.0", "gulp-uglify": "^3.0.2", "html5sortable": "^0.13.3", "jquery": "^3.6.0", - "konami": "^1.6.3", + "konami": "^1.7.0", "moment": "^2.29.4", - "node-sass": "^9.0.0", "popper.js": "^1.16.1", - "postcss": "^8.4.31", + "postcss": "^8.5.26", + "sass": "^1.103.1", "uglify-js": "^3.14.5" }, "devDependencies": { @@ -43,5 +43,9 @@ "gulp": "gulp", "build": "node node_modules/gulp/bin/gulp build", "heroku-cleanup": "rm -rf node_modules/ pipeline/source_assets gulpfile.js" + }, + "overrides": { + "postcss": "^8.5.26", + "immutable": "^4.3.9" } } diff --git a/pipeline/source_assets/js/asteroids.min.js b/pipeline/source_assets/js/asteroids.min.js index 3ad0aa9c..6f7ad494 100644 --- a/pipeline/source_assets/js/asteroids.min.js +++ b/pipeline/source_assets/js/asteroids.min.js @@ -1 +1 @@ -!function(){function Asteroids(){function Vector(e,t){"Object"==typeof e?(this.x=e.x,this.y=e.y):(this.x=e,this.y=t)}function Line(e,t){this.p1=e,this.p2=t}window.ASTEROIDS||(window.ASTEROIDS={enemiesKilled:0}),Vector.prototype={cp:function(){return new Vector(this.x,this.y)},mul:function(e){return this.x*=e,this.y*=e,this},mulNew:function(e){return new Vector(this.x*e,this.y*e)},add:function(e){return this.x+=e.x,this.y+=e.y,this},addNew:function(e){return new Vector(this.x+e.x,this.y+e.y)},sub:function(e){return this.x-=e.x,this.y-=e.y,this},subNew:function(e){return new Vector(this.x-e.x,this.y-e.y)},rotate:function(e){var t=this.x,i=this.y;return this.x=t*Math.cos(e)-Math.sin(e)*i,this.y=t*Math.sin(e)+Math.cos(e)*i,this},rotateNew:function(e){return this.cp().rotate(e)},setAngle:function(e){var t=this.len();return this.x=Math.cos(e)*t,this.y=Math.sin(e)*t,this},setAngleNew:function(e){return this.cp().setAngle(e)},setLength:function(e){var t=this.len();return t?this.mul(e/t):this.x=this.y=e,this},setLengthNew:function(e){return this.cp().setLength(e)},normalize:function(){var e=this.len();return this.x/=e,this.y/=e,this},normalizeNew:function(){return this.cp().normalize()},angle:function(){return Math.atan2(this.y,this.x)},collidesWith:function(e){return this.x>e.x&&this.y>e.y&&this.x-.005?0:e},is:function(e){return"object"==typeof e&&this.x==e.x&&this.y==e.y},toString:function(){return"[Vector("+this.x+", "+this.y+") angle: "+this.angle()+", length: "+this.len()+"]"}},Line.prototype={shift:function(e){this.p1.add(e),this.p2.add(e)},intersectsWithRect:function(e){var t=new Vector(e.x,e.y+e.height),i=new Vector(e.x,e.y),s=new Vector(e.x+e.width,e.y+e.height),n=new Vector(e.x+e.width,e.y);return this.p1.x>t.x&&this.p1.xn.y&&this.p2.x>t.x&&this.p2.xn.y||(!!this.intersectsLine(new Line(i,t))||(!!this.intersectsLine(new Line(t,s))||(!!this.intersectsLine(new Line(i,n))||!!this.intersectsLine(new Line(n,s)))))},intersectsLine:function(e){var t=this.p1,i=this.p2,s=e.p1,n=e.p2,a=(n.y-s.y)*(i.x-t.x)-(n.x-s.x)*(i.y-t.y),o=(n.x-s.x)*(t.y-s.y)-(n.y-s.y)*(t.x-s.x),h=(i.x-t.x)*(t.y-s.y)-(i.y-t.y)*(t.x-s.x);if(0==a)return!1;var r=o/a,l=h/a;return r>=0&&r<=1&&l>=0&&l<=1}};var that=this,isIE=!!window.ActiveXObject,isIEQuirks=isIE&&"BackCompat"==document.compatMode,w=document.documentElement.clientWidth,h=document.documentElement.clientHeight;isIEQuirks&&(w=document.body.clientWidth,h=document.body.clientHeight);var playerWidth=20,playerHeight=30,playerVerts=[[-1*playerHeight/2,-1*playerWidth/2],[-1*playerHeight/2,playerWidth/2],[playerHeight/2,0]],ignoredTypes=["HTML","HEAD","BODY","SCRIPT","TITLE","META","STYLE","LINK"];window.ActiveXObject&&(ignoredTypes=["HTML","HEAD","BODY","SCRIPT","TITLE","META","STYLE","LINK","SHAPE","LINE","GROUP","IMAGE","STROKE","FILL","SKEW","PATH","TEXTPATH","INS"]);var hiddenTypes=["BR","HR"],FPS=50,acc=300,maxSpeed=600,rotSpeed=360,bulletSpeed=700,particleSpeed=400,timeBetweenFire=150,timeBetweenBlink=250,timeBetweenEnemyUpdate=isIE?1e4:2e3,bulletRadius=2,maxParticles=isIE?20:40,maxBullets=isIE?10:20,createFlames,rWidth,rIncrease,yWidth,yIncrease,halfR,halfY,halfPlayerHeight;function updateEnemyIndex(){for(var e,t=0;e=that.enemies[t];t++)removeClass(e,"ASTEROIDSYEAHENEMY");var i=document.body.getElementsByTagName("*");that.enemies=[];var s;for(t=0;s=i[t];t++)-1==indexOf(ignoredTypes,s.tagName.toUpperCase())&&"g_vml_"!=s.prefix&&hasOnlyTextualChildren(s)&&"ASTEROIDSYEAH"!=s.className&&s.offsetHeight>0&&(s.aSize=size(s),that.enemies.push(s),addClass(s,"ASTEROIDSYEAHENEMY"),s.aAdded||(s.aAdded=!0,that.totalEnemies++))}function radians(e){return.0174532925*e}function degrees(e){return 57.2957795*e}function random(e,t){return Math.floor(Math.random()*(t+1)+e)}function code(e){var t={up:38,down:40,left:37,right:39,esc:27};return t[e]?t[e]:e.charCodeAt(0)}function boundsCheck(e){e.x>w?e.x=0:e.x<0&&(e.x=w),e.y>h?e.y=0:e.y<0&&(e.y=h)}function size(e){var t=e,i=0,s=0;do{i+=t.offsetLeft||0,s+=t.offsetTop||0,t=t.offsetParent}while(t);return{x:i,y:s,width:e.offsetWidth||10,height:e.offsetHeight||10}}function addEvent(e,t,i){e.addEventListener?e.addEventListener(t,i,!1):e.attachEvent&&(e["e"+t+i]=i,e[t+i]=function(){e["e"+t+i](window.event)},e.attachEvent("on"+t,e[t+i]))}function removeEvent(e,t,i){e.removeEventListener?e.removeEventListener(t,i,!1):e.detachEvent&&(e.detachEvent("on"+t,e[t+i]),e[t+i]=null,e["e"+t+i]=null)}function arrayRemove(e,t,i){var s=e.slice((i||t)+1||e.length);return e.length=t<0?e.length+t:t,e.push.apply(e,s)}function applyVisibility(e){for(var t,i=0;t=window.ASTEROIDSPLAYERS[i];i++)t.gameContainer.style.visibility=e}function getElementFromPoint(e,t){applyVisibility("hidden");var i=document.elementFromPoint(e,t);return i?(3==i.nodeType&&(i=i.parentNode),applyVisibility("visible"),i):(applyVisibility("visible"),!1)}function addParticles(e){for(var t=(new Date).getTime(),i=maxParticles,s=0;s0&&e.offsetHeight>0)return!1;if(-1!=indexOf(hiddenTypes,e.tagName))return!0;if(0==e.offsetWidth&&0==e.offsetHeight)return!1;for(var t=0;t50&&(createFlames(),this.updated.flame=t),this.scrollPos.x=window.pageXOffset||document.documentElement.scrollLeft,this.scrollPos.y=window.pageYOffset||document.documentElement.scrollTop,this.keysPressed[code("up")]||this.keysPressed[code("W")]?(this.vel.add(this.dir.mulNew(acc*i)),s=!0):this.vel.mul(.96),(this.keysPressed[code("left")]||this.keysPressed[code("A")])&&(e=!0,this.dir.rotate(radians(rotSpeed*i*-1))),(this.keysPressed[code("right")]||this.keysPressed[code("D")])&&(e=!0,this.dir.rotate(radians(rotSpeed*i))),this.keysPressed[code(" ")]&&t-this.firedAt>timeBetweenFire&&(this.bullets.unshift({dir:this.dir.cp(),pos:this.pos.cp(),startVel:this.vel.cp(),cameAlive:t}),this.firedAt=t,this.bullets.length>maxBullets&&this.bullets.pop()),this.keysPressed[code("B")]?(this.updated.enemies||(updateEnemyIndex(),this.updated.enemies=!0),e=!0,this.updated.blink.time+=1e3*i,this.updated.blink.time>timeBetweenBlink&&(this.toggleBlinkStyle(),this.updated.blink.time=0)):this.updated.enemies=!1,this.keysPressed[code("esc")])destroy.apply(this);else{this.vel.len()>maxSpeed&&this.vel.setLength(maxSpeed),this.pos.add(this.vel.mulNew(i)),this.pos.x>w?(window.scrollTo(this.scrollPos.x+50,this.scrollPos.y),this.pos.x=0):this.pos.x<0&&(window.scrollTo(this.scrollPos.x-50,this.scrollPos.y),this.pos.x=w),this.pos.y>h?(window.scrollTo(this.scrollPos.x,this.scrollPos.y+.75*h),this.pos.y=0):this.pos.y<0&&(window.scrollTo(this.scrollPos.x,this.scrollPos.y-.75*h),this.pos.y=h);for(var n=this.bullets.length-1;n>=0;n--)if(t-this.bullets[n].cameAlive>2e3)this.bullets.splice(n,1),e=!0;else{var a=this.bullets[n].dir.setLengthNew(bulletSpeed*i).add(this.bullets[n].startVel.mulNew(i));this.bullets[n].pos.add(a),boundsCheck(this.bullets[n].pos);var o=getElementFromPoint(this.bullets[n].pos.x,this.bullets[n].pos.y);o&&o.tagName&&-1==indexOf(ignoredTypes,o.tagName.toUpperCase())&&hasOnlyTextualChildren(o)&&"ASTEROIDSYEAH"!=o.className&&(didKill=!0,addParticles(this.bullets[n].pos),this.dying.push(o),this.bullets.splice(n,1))}if(this.dying.length){for(n=this.dying.length-1;n>=0;n--)try{this.dying[n].parentNode&&window.ASTEROIDS.enemiesKilled++,this.dying[n].parentNode.removeChild(this.dying[n])}catch(e){}setScore(),this.dying=[]}for(n=this.particles.length-1;n>=0;n--)this.particles[n].pos.add(this.particles[n].dir.mulNew(particleSpeed*i*Math.random())),t-this.particles[n].cameAlive>1e3&&(this.particles.splice(n,1),e=!0);isIEQuirks&&(this.gameContainer.style.left=this.canvas.style.left=document.documentElement.scrollLeft+"px",this.gameContainer.style.top=this.canvas.style.top=document.documentElement.scrollTop+"px",this.navigation.style.right="10px",this.navigation.style.top=document.documentElement.scrollTop+document.body.clientHeight-this.navigation.clientHeight-10+"px"),(e||0!=this.bullets.length||0!=this.particles.length||!this.pos.is(this.lastPos)||this.vel.len()>0)&&(this.ctx.clear(),this.ctx.drawPlayer(),s&&this.ctx.drawFlames(that.flame),this.bullets.length&&this.ctx.drawBullets(this.bullets),this.particles.length&&this.ctx.drawParticles(this.particles)),this.lastPos=this.pos}};var updateFunc=function(){try{that.update.call(that)}catch(e){throw clearInterval(interval),e}},interval=setInterval(updateFunc,1e3/FPS);function destroy(){removeEvent(document,"keydown",eventKeydown),removeEvent(document,"keypress",eventKeypress),removeEvent(document,"keyup",eventKeyup),removeEvent(window,"resize",eventResize),isRunning=!1,removeStylesheet("ASTEROIDSYEAHSTYLES"),removeClass(document.body,"ASTEROIDSYEAH"),this.gameContainer.parentNode.removeChild(this.gameContainer)}}if(window.ASTEROIDSPLAYERS||(window.ASTEROIDSPLAYERS=[]),window.ActiveXObject&&!document.createElement("canvas").getContext){try{var xamlScript=document.createElement("script");xamlScript.setAttribute("type","text/xaml"),xamlScript.textContent='',document.getElementsByTagName("head")[0].appendChild(xamlScript)}catch(e){}var script=document.createElement("script");script.setAttribute("type","text/javascript"),script.onreadystatechange=function(){"loaded"!=script.readyState&&"complete"!=script.readyState||"undefined"!=typeof G_vmlCanvasManager&&(window.ASTEROIDSPLAYERS[window.ASTEROIDSPLAYERS.length]=new Asteroids)},script.src="http://erkie.github.com/excanvas.js",document.getElementsByTagName("head")[0].appendChild(script)}else window.ASTEROIDSPLAYERS[window.ASTEROIDSPLAYERS.length]=new Asteroids}(); \ No newline at end of file +!function(){function Asteroids(){function Vector(e,t){"Object"==typeof e?(this.x=e.x,this.y=e.y):(this.x=e,this.y=t)}function Line(e,t){this.p1=e,this.p2=t}window.ASTEROIDS||(window.ASTEROIDS={enemiesKilled:0}),Vector.prototype={cp:function(){return new Vector(this.x,this.y)},mul:function(e){return this.x*=e,this.y*=e,this},mulNew:function(e){return new Vector(this.x*e,this.y*e)},add:function(e){return this.x+=e.x,this.y+=e.y,this},addNew:function(e){return new Vector(this.x+e.x,this.y+e.y)},sub:function(e){return this.x-=e.x,this.y-=e.y,this},subNew:function(e){return new Vector(this.x-e.x,this.y-e.y)},rotate:function(e){var t=this.x,i=this.y;return this.x=t*Math.cos(e)-Math.sin(e)*i,this.y=t*Math.sin(e)+Math.cos(e)*i,this},rotateNew:function(e){return this.cp().rotate(e)},setAngle:function(e){var t=this.len();return this.x=Math.cos(e)*t,this.y=Math.sin(e)*t,this},setAngleNew:function(e){return this.cp().setAngle(e)},setLength:function(e){var t=this.len();return t?this.mul(e/t):this.x=this.y=e,this},setLengthNew:function(e){return this.cp().setLength(e)},normalize:function(){var e=this.len();return this.x/=e,this.y/=e,this},normalizeNew:function(){return this.cp().normalize()},angle:function(){return Math.atan2(this.y,this.x)},collidesWith:function(e){return this.x>e.x&&this.y>e.y&&this.x-.005?0:e},is:function(e){return"object"==typeof e&&this.x==e.x&&this.y==e.y},toString:function(){return"[Vector("+this.x+", "+this.y+") angle: "+this.angle()+", length: "+this.len()+"]"}},Line.prototype={shift:function(e){this.p1.add(e),this.p2.add(e)},intersectsWithRect:function(e){var t=new Vector(e.x,e.y+e.height),i=new Vector(e.x,e.y),s=new Vector(e.x+e.width,e.y+e.height),n=new Vector(e.x+e.width,e.y);return this.p1.x>t.x&&this.p1.xn.y&&this.p2.x>t.x&&this.p2.xn.y||(!!this.intersectsLine(new Line(i,t))||(!!this.intersectsLine(new Line(t,s))||(!!this.intersectsLine(new Line(i,n))||!!this.intersectsLine(new Line(n,s)))))},intersectsLine:function(e){var t=this.p1,i=this.p2,s=e.p1,n=e.p2,a=(n.y-s.y)*(i.x-t.x)-(n.x-s.x)*(i.y-t.y),o=(n.x-s.x)*(t.y-s.y)-(n.y-s.y)*(t.x-s.x),h=(i.x-t.x)*(t.y-s.y)-(i.y-t.y)*(t.x-s.x);if(0==a)return!1;var r=o/a,l=h/a;return r>=0&&r<=1&&l>=0&&l<=1}};var that=this,isIE=!!window.ActiveXObject,isIEQuirks=isIE&&"BackCompat"==document.compatMode,w=document.documentElement.clientWidth,h=document.documentElement.clientHeight;isIEQuirks&&(w=document.body.clientWidth,h=document.body.clientHeight);var playerWidth=20,playerHeight=30,playerVerts=[[-1*playerHeight/2,-1*playerWidth/2],[-1*playerHeight/2,playerWidth/2],[playerHeight/2,0]],ignoredTypes=["HTML","HEAD","BODY","SCRIPT","TITLE","META","STYLE","LINK"];window.ActiveXObject&&(ignoredTypes=["HTML","HEAD","BODY","SCRIPT","TITLE","META","STYLE","LINK","SHAPE","LINE","GROUP","IMAGE","STROKE","FILL","SKEW","PATH","TEXTPATH","INS"]);var hiddenTypes=["BR","HR"],FPS=50,acc=300,maxSpeed=600,rotSpeed=360,bulletSpeed=700,particleSpeed=400,timeBetweenFire=150,timeBetweenBlink=250,timeBetweenEnemyUpdate=isIE?1e4:2e3,bulletRadius=2,maxParticles=isIE?20:40,maxBullets=isIE?10:20,createFlames,rWidth,rIncrease,yWidth,yIncrease,halfR,halfY,halfPlayerHeight;function updateEnemyIndex(){for(var e,t=0;e=that.enemies[t];t++)removeClass(e,"ASTEROIDSYEAHENEMY");var i=document.body.getElementsByTagName("*");that.enemies=[];var s;for(t=0;s=i[t];t++)-1==indexOf(ignoredTypes,s.tagName.toUpperCase())&&"g_vml_"!=s.prefix&&hasOnlyTextualChildren(s)&&"ASTEROIDSYEAH"!=s.className&&s.offsetHeight>0&&(s.aSize=size(s),that.enemies.push(s),addClass(s,"ASTEROIDSYEAHENEMY"),s.aAdded||(s.aAdded=!0,that.totalEnemies++))}function radians(e){return.0174532925*e}function degrees(e){return 57.2957795*e}function random(e,t){return Math.floor(Math.random()*(t+1)+e)}function code(e){var t={up:38,down:40,left:37,right:39,esc:27};return t[e]?t[e]:e.charCodeAt(0)}function boundsCheck(e){e.x>w?e.x=0:e.x<0&&(e.x=w),e.y>h?e.y=0:e.y<0&&(e.y=h)}function size(e){var t=e,i=0,s=0;do{i+=t.offsetLeft||0,s+=t.offsetTop||0,t=t.offsetParent}while(t);return{x:i,y:s,width:e.offsetWidth||10,height:e.offsetHeight||10}}function addEvent(e,t,i){e.addEventListener?e.addEventListener(t,i,!1):e.attachEvent&&(e["e"+t+i]=i,e[t+i]=function(){e["e"+t+i](window.event)},e.attachEvent("on"+t,e[t+i]))}function removeEvent(e,t,i){e.removeEventListener?e.removeEventListener(t,i,!1):e.detachEvent&&(e.detachEvent("on"+t,e[t+i]),e[t+i]=null,e["e"+t+i]=null)}function arrayRemove(e,t,i){var s=e.slice((i||t)+1||e.length);return e.length=t<0?e.length+t:t,e.push.apply(e,s)}function applyVisibility(e){for(var t,i=0;t=window.ASTEROIDSPLAYERS[i];i++)t.gameContainer.style.visibility=e}function getElementFromPoint(e,t){applyVisibility("hidden");var i=document.elementFromPoint(e,t);return i?(3==i.nodeType&&(i=i.parentNode),applyVisibility("visible"),i):(applyVisibility("visible"),!1)}function addParticles(e){for(var t=(new Date).getTime(),i=maxParticles,s=0;s0&&e.offsetHeight>0)return!1;if(-1!=indexOf(hiddenTypes,e.tagName))return!0;if(0==e.offsetWidth&&0==e.offsetHeight)return!1;for(var t=0;t50&&(createFlames(),this.updated.flame=t),this.scrollPos.x=window.pageXOffset||document.documentElement.scrollLeft,this.scrollPos.y=window.pageYOffset||document.documentElement.scrollTop,this.keysPressed[code("up")]||this.keysPressed[code("W")]?(this.vel.add(this.dir.mulNew(acc*i)),s=!0):this.vel.mul(.96),(this.keysPressed[code("left")]||this.keysPressed[code("A")])&&(e=!0,this.dir.rotate(radians(rotSpeed*i*-1))),(this.keysPressed[code("right")]||this.keysPressed[code("D")])&&(e=!0,this.dir.rotate(radians(rotSpeed*i))),this.keysPressed[code(" ")]&&t-this.firedAt>timeBetweenFire&&(this.bullets.unshift({dir:this.dir.cp(),pos:this.pos.cp(),startVel:this.vel.cp(),cameAlive:t}),this.firedAt=t,this.bullets.length>maxBullets&&this.bullets.pop()),this.keysPressed[code("B")]?(this.updated.enemies||(updateEnemyIndex(),this.updated.enemies=!0),e=!0,this.updated.blink.time+=1e3*i,this.updated.blink.time>timeBetweenBlink&&(this.toggleBlinkStyle(),this.updated.blink.time=0)):this.updated.enemies=!1,this.keysPressed[code("esc")])destroy.apply(this);else{this.vel.len()>maxSpeed&&this.vel.setLength(maxSpeed),this.pos.add(this.vel.mulNew(i)),this.pos.x>w?(window.scrollTo(this.scrollPos.x+50,this.scrollPos.y),this.pos.x=0):this.pos.x<0&&(window.scrollTo(this.scrollPos.x-50,this.scrollPos.y),this.pos.x=w),this.pos.y>h?(window.scrollTo(this.scrollPos.x,this.scrollPos.y+.75*h),this.pos.y=0):this.pos.y<0&&(window.scrollTo(this.scrollPos.x,this.scrollPos.y-.75*h),this.pos.y=h);for(var n=this.bullets.length-1;n>=0;n--)if(t-this.bullets[n].cameAlive>2e3)this.bullets.splice(n,1),e=!0;else{var a=this.bullets[n].dir.setLengthNew(bulletSpeed*i).add(this.bullets[n].startVel.mulNew(i));this.bullets[n].pos.add(a),boundsCheck(this.bullets[n].pos);var o=getElementFromPoint(this.bullets[n].pos.x,this.bullets[n].pos.y);o&&o.tagName&&-1==indexOf(ignoredTypes,o.tagName.toUpperCase())&&hasOnlyTextualChildren(o)&&"ASTEROIDSYEAH"!=o.className&&(didKill=!0,addParticles(this.bullets[n].pos),this.dying.push(o),this.bullets.splice(n,1))}if(this.dying.length){for(n=this.dying.length-1;n>=0;n--)try{this.dying[n].parentNode&&window.ASTEROIDS.enemiesKilled++,this.dying[n].parentNode.removeChild(this.dying[n])}catch(e){}setScore(),this.dying=[]}for(n=this.particles.length-1;n>=0;n--)this.particles[n].pos.add(this.particles[n].dir.mulNew(particleSpeed*i*Math.random())),t-this.particles[n].cameAlive>1e3&&(this.particles.splice(n,1),e=!0);isIEQuirks&&(this.gameContainer.style.left=this.canvas.style.left=document.documentElement.scrollLeft+"px",this.gameContainer.style.top=this.canvas.style.top=document.documentElement.scrollTop+"px",this.navigation.style.right="10px",this.navigation.style.top=document.documentElement.scrollTop+document.body.clientHeight-this.navigation.clientHeight-10+"px"),(e||0!=this.bullets.length||0!=this.particles.length||!this.pos.is(this.lastPos)||this.vel.len()>0)&&(this.ctx.clear(),this.ctx.drawPlayer(),s&&this.ctx.drawFlames(that.flame),this.bullets.length&&this.ctx.drawBullets(this.bullets),this.particles.length&&this.ctx.drawParticles(this.particles)),this.lastPos=this.pos}};var updateFunc=function(){try{that.update.call(that)}catch(e){throw clearInterval(interval),e}},interval=setInterval(updateFunc,1e3/FPS);function destroy(){removeEvent(document,"keydown",eventKeydown),removeEvent(document,"keypress",eventKeypress),removeEvent(document,"keyup",eventKeyup),removeEvent(window,"resize",eventResize),isRunning=!1,removeStylesheet("ASTEROIDSYEAHSTYLES"),removeClass(document.body,"ASTEROIDSYEAH"),this.gameContainer.parentNode.removeChild(this.gameContainer)}}if(window.ASTEROIDSPLAYERS||(window.ASTEROIDSPLAYERS=[]),window.ActiveXObject&&!document.createElement("canvas").getContext){try{var xamlScript=document.createElement("script");xamlScript.setAttribute("type","text/xaml"),xamlScript.textContent='',document.getElementsByTagName("head")[0].appendChild(xamlScript)}catch(e){}var script=document.createElement("script");script.setAttribute("type","text/javascript"),script.onreadystatechange=function(){"loaded"!=script.readyState&&"complete"!=script.readyState||"undefined"!=typeof G_vmlCanvasManager&&(window.ASTEROIDSPLAYERS[window.ASTEROIDSPLAYERS.length]=new Asteroids)},script.src="http://erkie.github.com/excanvas.js",document.getElementsByTagName("head")[0].appendChild(script)}else window.ASTEROIDSPLAYERS[window.ASTEROIDSPLAYERS.length]=new Asteroids}(); diff --git a/pipeline/source_assets/scss/email.scss b/pipeline/source_assets/scss/email.scss index cddee224..4061d59c 100644 --- a/pipeline/source_assets/scss/email.scss +++ b/pipeline/source_assets/scss/email.scss @@ -60,4 +60,3 @@ body{ } } - diff --git a/pyproject.toml b/pyproject.toml index 002ee663..45d55b9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,97 +3,63 @@ name = "pyrigs" version = "0.1.0" description = "A Django-based event booking system designed for use by TEC PA and Lighting" readme = "README.md" -requires-python = "~=3.10.0" +requires-python = "~=3.14.0" dependencies = [ - "ansicolors~=1.1.8", - "asgiref~=3.3.1", - "backports.tempfile~=1.0", - "backports.weakref~=1.0.post1", - "beautifulsoup4~=4.9.3", - "Brotli~=1.0.9", - "cachetools~=4.2.1", - "chardet~=4.0.0", - "configparser~=5.0.1", - "contextlib2~=0.6.0.post1", - "cssselect~=1.1.0", - "cssutils~=1.0.2", - "dj-database-url~=0.5.0", - "dj-static~=0.0.6", - "Django~=3.2", - "django-debug-toolbar~=4.0.0", - "django-filter~=2.4.0", - "django-ical~=1.7.1", - "django-recurrence~=1.10.3", - "django-registration-redux~=2.9", - "django-reversion~=3.0.9", - "django-widget-tweaks~=1.4.8", - "django-htmlmin~=0.11.0", - "envparse", - "gunicorn~=22.0.0", - "icalendar~=4.0.7", - "idna~=3.7", - "Markdown~=3.3.3", - "msgpack~=1.0.2", - "pep517~=0.9.1", - "Pillow~=10.0.1", - "premailer~=3.7.0", - "progress~=1.5", - "psutil~=5.8.0", - "psycopg2-binary", - "Pygments~=2.15.0", - "pyparsing~=2.4.7", - "PyPDF2~=1.27.5", - "PyPOM~=2.2.4", - "python-dateutil~=2.8.1", - "pytoml~=0.1.21", - "pytz~=2020.5", - "reportlab", - "requests~=2.32.3", - "retrying~=1.3.3", - "simplejson~=3.17.2", - "six~=1.15.0", - "soupsieve~=2.1", - "sqlparse~=0.5.0", - "static3~=0.7.0", - "svg2rlg~=0.3", - "tini~=3.0.1", - "tornado~=6.3", - "urllib3~=1.26.19", - "whitenoise~=5.2.0", - "yolk~=0.4.3", - "zipp~=3.4.0", - "zope.component~=4.6.2", - "zope.deferredimport~=4.3.1", - "zope.deprecation~=4.4.0", - "zope.event~=4.5.0", - "zope.hookable~=5.0.1", - "zope.proxy~=4.3.5", - "zope.schema~=6.0.1", - "sentry-sdk", - "diff-match-patch", - "python-barcode", - "django-hCaptcha", - "importlib-metadata", - "django-hcaptcha", - "z3c.rml", - "pikepdf", - "django-queryable-properties", - "django-mass-edit", - "selenium~=4.9.1", - "zope.interface", + "beautifulsoup4~=4.14", + "Brotli~=1.2", + "Django~=5.2", + "django-ical~=1.9", + "django-registration-redux~=2.13", + "django-reversion~=6.0", + "django-widget-tweaks~=1.5", + "django-htmlmin~=0.11", + "envparse~=0.2", + "gunicorn~=26.1", + "Markdown~=3.9", + "Pillow~=12.3", + "premailer~=3.10", + "psycopg2-binary~=2.9", + "pypdf~=6.19", + "pytz~=2025.2", + "reportlab~=4.4", + "simplejson~=3.20", + "whitenoise~=6.12", + "sentry-sdk~=2.68", + "diff-match-patch>=20241021,<20300000", + "python-barcode~=0.16", + "django-hcaptcha~=0.2", + "z3c.rml~=5.0", + "django-queryable-properties~=1.12", + "django-mass-edit~=3.5", ] [dependency-groups] dev = [ - "pycodestyle~=2.9.1", - "coveralls", - "django-coverage-plugin", - "pytest-cov", - "pytest-django", - "pluggy", - "pytest-splinter", - "pytest", - "pytest-reverse", - "pytest-xdist[psutil]", - "PyPOM[splinter]", + "coveralls~=4.0", + "django-coverage-plugin~=3.1", + "pytest-cov~=7.1", + "pytest-django~=4.14", + "pytest~=9.1", + "selenium~=4.35", + "pytest-reverse~=1.9", + "pytest-xdist[psutil]~=3.8", + "autopep8~=2.3", + "pip-audit~=2.10", + "ruff~=0.16", + "prek~=0.5", ] + +[tool.ruff] +line-length = 120 +extend-exclude = ["*/migrations/*"] + +[tool.ruff.lint] +select = ["E", "F", "W"] +# The formatter does not split long strings/templates, so don't fail on them +ignore = ["E501"] + +[tool.ruff.lint.per-file-ignores] +# Re-exports for the URLconf +"RIGS/views/__init__.py" = ["F403"] +# EventTest is not collected by pytest (it isn't prefixed with Test) and references names that don't exist +"RIGS/tests/test_models.py" = ["F821"] diff --git a/scripts/certbot-issue.sh b/scripts/certbot-issue.sh new file mode 100644 index 00000000..4cdbcb88 --- /dev/null +++ b/scripts/certbot-issue.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Issue the initial certificate (run as root on the deployment server) +# Usage: sudo ./scripts/certbot-issue.sh +set -euo pipefail + +DOMAIN="${1:?Usage: $0 }" +EMAIL="${2:?Usage: $0 }" + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +WEBROOT="$REPO_DIR/nginx/certbot" +CERTS_DIR="$REPO_DIR/nginx/certs" +HOOK_PATH="/etc/letsencrypt/renewal-hooks/deploy/pyrigs-deploy.sh" + +if ! command -v certbot >/dev/null 2>&1; then + echo "certbot not found. Install it first: apt install certbot (or snap install certbot --classic)" >&2 + exit 1 +fi + +mkdir -p "$WEBROOT" "$CERTS_DIR" /etc/letsencrypt/renewal-hooks/deploy + +# 1. Initial issuance (certbot skips if the cert already exists and is not due for renewal) +certbot certonly \ + --webroot -w "$WEBROOT" \ + -d "$DOMAIN" \ + -m "$EMAIL" \ + --agree-tos --no-eff-email --non-interactive + +# 2. Install deploy-hook (placed in the official dir, triggered automatically on successful renewal) +cat > "$HOOK_PATH" <&2 +fi +echo "pyrigs deploy-hook: \$(date) renewed [\$RENEWED_DOMAINS] and reloaded nginx" +EOF +chmod +x "$HOOK_PATH" + +# 3. Deploy the freshly issued certificate right away +RENEWED_LINEAGE="/etc/letsencrypt/live/$DOMAIN" \ +RENEWED_DOMAINS="$DOMAIN" \ + "$HOOK_PATH" + +echo "Done: certificate for $DOMAIN issued and deployed to $CERTS_DIR; deploy-hook installed." +echo "Tip: for scheduled renewal, run sudo ./scripts/cron-install.sh" diff --git a/scripts/cron-install.sh b/scripts/cron-install.sh new file mode 100644 index 00000000..c8708d35 --- /dev/null +++ b/scripts/cron-install.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Install system-level scheduled tasks (run as root on the deployment server) +# Usage: sudo ./scripts/cron-install.sh +# +# Writes two /etc/cron.d files: +# 1. pyrigs-certbot - certificate renewal check, daily at 03:00 / 15:00 +# 2. pyrigs-django - cleanup and reminder jobs inside the pyrigs container +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CERTBOT_CRON="/etc/cron.d/pyrigs-certbot" +DJANGO_CRON="/etc/cron.d/pyrigs-django" + +# 1. Certificate renewal: checked twice a day (officially recommended frequency); +# on successful renewal the deploy-hook in the official hook dir takes over +cat > "$CERTBOT_CRON" < "$DJANGO_CRON" <> /var/log/pyrigs-cleanup.log 2>&1 +0 8 * * * root docker compose -f $REPO_DIR/compose.yml exec -T pyrigs python manage.py send_reminders >> /var/log/pyrigs-reminders.log 2>&1 +EOF + +chmod 644 "$CERTBOT_CRON" "$DJANGO_CRON" + +echo "Installed:" +echo " $CERTBOT_CRON" +echo " $DJANGO_CRON" diff --git a/scripts/cron-uninstall.sh b/scripts/cron-uninstall.sh new file mode 100644 index 00000000..9347bd9d --- /dev/null +++ b/scripts/cron-uninstall.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Uninstall the scheduled tasks installed by cron-install.sh (run as root on the deployment server) +# Usage: sudo ./scripts/cron-uninstall.sh +set -euo pipefail + +CERTBOT_CRON="/etc/cron.d/pyrigs-certbot" +DJANGO_CRON="/etc/cron.d/pyrigs-django" +HOOK_PATH="/etc/letsencrypt/renewal-hooks/deploy/pyrigs-deploy.sh" + +rm -f "$CERTBOT_CRON" "$DJANGO_CRON" +echo "Removed scheduled tasks: $CERTBOT_CRON $DJANGO_CRON" + +if [ -f "$HOOK_PATH" ]; then + read -rp "Also remove the deploy-hook ($HOOK_PATH)? [y/N] " ans + case "$ans" in + y|Y) rm -f "$HOOK_PATH"; echo "Deploy-hook removed" ;; + *) echo "Deploy-hook kept" ;; + esac +fi diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d5be33a9..00000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[pycodestyle] -max-line-length = 320 -exclude = .venv diff --git a/templates/base.html b/templates/base.html index 2b79afe6..a7add77b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -76,7 +76,7 @@ {% endfor %} {% endif %} {% endblock %} - {% if page_title and not request.is_ajax %} + {% if page_title and not is_ajax %}

    {{page_title|safe}}

    {% endif %} {% block content %}{% endblock %} diff --git a/templates/base_ajax_nomodal.html b/templates/base_ajax_nomodal.html index aaaa06ed..7094370c 100644 --- a/templates/base_ajax_nomodal.html +++ b/templates/base_ajax_nomodal.html @@ -1,2 +1,2 @@ {% block js %}{% endblock %} -{% block content %}{% endblock %} \ No newline at end of file +{% block content %}{% endblock %} diff --git a/templates/closemodal.html b/templates/closemodal.html index 56f2e90d..5cd52641 100644 --- a/templates/closemodal.html +++ b/templates/closemodal.html @@ -5,4 +5,4 @@ {% endif %} \ No newline at end of file + diff --git a/templates/generic_detail.html b/templates/generic_detail.html index 73526520..b146e085 100644 --- a/templates/generic_detail.html +++ b/templates/generic_detail.html @@ -46,7 +46,7 @@ {% include associated2|safe %} {% endif %} - {% if not request.is_ajax %} + {% if not is_ajax %}
    {% if can_edit %} @@ -59,7 +59,7 @@
    {% endif %} {% endblock %} -{% if request.is_ajax %} +{% if is_ajax %} {% block footer %}
    diff --git a/templates/pagination.html b/templates/pagination.html index 1205c102..4a805246 100644 --- a/templates/pagination.html +++ b/templates/pagination.html @@ -17,11 +17,11 @@ {% endif %} {% for page in page_numbers %} - {% ifequal page page_obj.number %} + {% if page == page_obj.number %}
  • {{ page }}
  • {% else %}
  • {{ page }}
  • - {% endifequal %} + {% endif %} {% endfor %} {% if show_last %} diff --git a/templates/registration/logged_out.html b/templates/registration/logged_out.html index 058e2127..d07f5e3e 100644 --- a/templates/registration/logged_out.html +++ b/templates/registration/logged_out.html @@ -7,4 +7,4 @@

    Logout Successful

    You have successfully been logged out of RIGS

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/registration/password_change_done.html b/templates/registration/password_change_done.html index e306a5fa..b44660f1 100644 --- a/templates/registration/password_change_done.html +++ b/templates/registration/password_change_done.html @@ -14,7 +14,7 @@ {% block content %}

    {% trans 'Password change successful' %}

    - +

    {% trans "Your password has been changed" %}

    {% endblock %} diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html index 88bc4d77..687d9c85 100644 --- a/templates/registration/password_reset_complete.html +++ b/templates/registration/password_reset_complete.html @@ -14,9 +14,9 @@ {% block content %}

    {% trans 'Password reset complete' %}

    - +

    {% trans "Your password has been set. You may go ahead and log in now." %}

    - +

    {% trans 'Log in' %}

    diff --git a/templates/registration/password_reset_confirm.html b/templates/registration/password_reset_confirm.html index 8b8a8112..3c3e1664 100644 --- a/templates/registration/password_reset_confirm.html +++ b/templates/registration/password_reset_confirm.html @@ -18,9 +18,9 @@

    {% trans 'Enter new password' %}

    - +

    {% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

    - +
    {% csrf_token %} {% if form.errors %} @@ -47,13 +47,13 @@
    - + {% else %} - +

    {% trans 'Password reset unsuccessful' %}

    - +

    {% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

    - +
    {% endif %} diff --git a/templates/registration/password_reset_done.html b/templates/registration/password_reset_done.html index 062cba6f..1f6c46f0 100644 --- a/templates/registration/password_reset_done.html +++ b/templates/registration/password_reset_done.html @@ -14,7 +14,7 @@ {% block content %}

    {% trans 'Password reset successful' %}

    - +

    {% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}

    {% endblock %} diff --git a/templates/registration/password_reset_form.html b/templates/registration/password_reset_form.html index 5e71546d..36386212 100644 --- a/templates/registration/password_reset_form.html +++ b/templates/registration/password_reset_form.html @@ -7,9 +7,9 @@ {% block content %}

    Password Reset

    - +

    {% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}

    - +
    {% csrf_token %} {% if form.errors %} @@ -32,4 +32,4 @@
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/registration/registration_complete.html b/templates/registration/registration_complete.html index f065ff21..d43ea3be 100644 --- a/templates/registration/registration_complete.html +++ b/templates/registration/registration_complete.html @@ -7,4 +7,4 @@

    Thanks for registering

    Thanks for registering with RIGS, further details will be emailed to you.

    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/search_help.html b/templates/search_help.html index 41ec63c3..2a11bf60 100644 --- a/templates/search_help.html +++ b/templates/search_help.html @@ -1,10 +1,10 @@ -{% extends request.is_ajax|yesno:"base_ajax.html,base.html" %} +{% extends is_ajax|yesno:"base_ajax.html,base.html" %} {% block title %}Search Help{% endblock %} {% block content %}
    - {% if not request.is_ajax %} + {% if not is_ajax %}

    Search Help

    diff --git a/training/admin.py b/training/admin.py index 80bf1e03..6493031a 100644 --- a/training/admin.py +++ b/training/admin.py @@ -12,4 +12,4 @@ admin.site.register(models.TrainingLevelRequirement, VersionAdmin) @admin.register(models.TrainingItemQualification) class TrainingItemQualificationAdmin(VersionAdmin): - list_display = ['__str__', 'trainee'] + list_display = ["__str__", "trainee"] diff --git a/training/apps.py b/training/apps.py index a94e5537..801c4220 100644 --- a/training/apps.py +++ b/training/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class TrainingConfig(AppConfig): - name = 'training' + name = "training" diff --git a/training/decorators.py b/training/decorators.py index c56f9470..13c66394 100644 --- a/training/decorators.py +++ b/training/decorators.py @@ -2,4 +2,4 @@ from PyRIGS.decorators import user_passes_test_with_403 def is_supervisor(login_url=None, oembed_view=None): - return user_passes_test_with_403(lambda u: (hasattr(u, 'is_supervisor') and u.is_supervisor)) + return user_passes_test_with_403(lambda u: hasattr(u, "is_supervisor") and u.is_supervisor) diff --git a/training/forms.py b/training/forms.py index c97b3c78..771d1c2a 100644 --- a/training/forms.py +++ b/training/forms.py @@ -6,55 +6,50 @@ from training import models class QualificationForm(forms.ModelForm): - related_models = { - 'item': models.TrainingItem, - 'supervisor': models.Trainee - } + related_models = {"item": models.TrainingItem, "supervisor": models.Trainee} class Meta: model = models.TrainingItemQualification - fields = '__all__' + fields = "__all__" def clean_date(self): - date = self.cleaned_data.get('date') + date = self.cleaned_data.get("date") if date > date.today(): - raise forms.ValidationError('Qualification date may not be in the future') + raise forms.ValidationError("Qualification date may not be in the future") return date def clean_supervisor(self): - supervisor = self.cleaned_data.get('supervisor') - if supervisor.pk == self.cleaned_data.get('trainee').pk: - raise forms.ValidationError('One may not supervise oneself...') + supervisor = self.cleaned_data.get("supervisor") + if supervisor.pk == self.cleaned_data.get("trainee").pk: + raise forms.ValidationError("One may not supervise oneself...") return supervisor def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['date'].widget.format = '%Y-%m-%d' + self.fields["date"].widget.format = "%Y-%m-%d" class AddQualificationForm(QualificationForm): def __init__(self, *args, **kwargs): - pk = kwargs.pop('pk', None) + pk = kwargs.pop("pk", None) super().__init__(*args, **kwargs) if pk: - self.fields['trainee'].initial = models.Trainee.objects.get(pk=pk) + self.fields["trainee"].initial = models.Trainee.objects.get(pk=pk) class RequirementForm(forms.ModelForm): - related_models = { - 'item': models.TrainingItem - } + related_models = {"item": models.TrainingItem} depth = forms.ChoiceField(choices=models.TrainingItemQualification.CHOICES) class Meta: model = models.TrainingLevelRequirement - fields = '__all__' + fields = "__all__" def __init__(self, *args, **kwargs): - pk = kwargs.pop('pk', None) + pk = kwargs.pop("pk", None) super().__init__(*args, **kwargs) - self.fields['level'].initial = models.TrainingLevel.objects.get(pk=pk) + self.fields["level"].initial = models.TrainingLevel.objects.get(pk=pk) class SessionLogForm(forms.Form): @@ -66,15 +61,13 @@ class SessionLogForm(forms.Form): date = forms.DateField(initial=datetime.date.today) notes = forms.CharField(required=False, widget=forms.Textarea) - related_models = { - 'supervisor': models.Trainee - } + related_models = {"supervisor": models.Trainee} def clean_date(self): return QualificationForm.clean_date(self) def clean_supervisor(self): - supervisor = self.cleaned_data['supervisor'] - if supervisor in self.cleaned_data.get('trainees', []): - raise forms.ValidationError('One may not supervise oneself...') + supervisor = self.cleaned_data["supervisor"] + if supervisor in self.cleaned_data.get("trainees", []): + raise forms.ValidationError("One may not supervise oneself...") return supervisor diff --git a/training/management/commands/generateSampleTrainingData.py b/training/management/commands/generateSampleTrainingData.py index f342c023..1017a9b9 100644 --- a/training/management/commands/generateSampleTrainingData.py +++ b/training/management/commands/generateSampleTrainingData.py @@ -1,18 +1,13 @@ -import datetime import random -from django.contrib.auth.models import Group, Permission -from django.core.management import call_command from django.core.management.base import BaseCommand, CommandError from django.db import transaction -from django.utils import timezone -from reversion import revisions as reversion from training import models class Command(BaseCommand): - help = 'Adds sample data to use for testing' + help = "Adds sample data to use for testing" can_import_settings = True categories = [] @@ -24,9 +19,9 @@ class Command(BaseCommand): from django.conf import settings if not (settings.DEBUG or settings.STAGING): - raise CommandError('You cannot run this command in production') + raise CommandError("You cannot run this command in production") - random.seed('otherwise it is done by time, which could lead to inconsistant tests') + random.seed("otherwise it is done by time, which could lead to inconsistant tests") with transaction.atomic(): self.setup_categories() @@ -141,7 +136,8 @@ class Command(BaseCommand): "Losing Crimpaz", "Scrapping Trilite", "Bin Diving", - "Wiki Editing"] + "Wiki Editing", + ] descriptions = [ "Physical training concentrates on mechanistic goals: training programs in this area develop specific motor skills, agility, strength or physical fitness, often with an intention of peaking at a particular time.", @@ -149,7 +145,7 @@ class Command(BaseCommand): "These include how to use a variety of weapons, outdoor survival skills, and how to survive being captured by the enemy, among many others. See military education and training.", "While some studies have indicated relaxation training is useful for some medical conditions, autogenic training has limited results or has been the result of few studies.", "Some occupations are inherently hazardous, and require a minimum level of competence before the practitioners can perform the work at an acceptable level of safety to themselves or others in the vicinity.", - "Occupational diving, rescue, firefighting and operation of certain types of machinery and vehicles may require assessment and certification of a minimum acceptable competence before the person is allowed to practice as a licensed instructor." + "Occupational diving, rescue, firefighting and operation of certain types of machinery and vehicles may require assessment and certification of a minimum acceptable competence before the person is allowed to practice as a licensed instructor.", ] for i, name in enumerate(names): @@ -159,7 +155,12 @@ class Command(BaseCommand): number = previous_item.reference_number + 1 else: number = 0 - item = models.TrainingItem.objects.create(category=category, reference_number=number, name=name, description=random.choice(descriptions) + random.choice(descriptions) + random.choice(descriptions)) + item = models.TrainingItem.objects.create( + category=category, + reference_number=number, + name=name, + description=random.choice(descriptions) + random.choice(descriptions) + random.choice(descriptions), + ) self.items.append(item) def setup_levels(self): @@ -167,34 +168,54 @@ class Command(BaseCommand): ta = models.TrainingLevel.objects.create( level=models.TrainingLevel.TA, description="Passion will hatred faithful evil suicide noble battle. Truth aversion gains grandeur noble. Dead play gains prejudice god ascetic grandeur zarathustra dead good. Faithful ultimate justice overcome love will mountains inexpedient.", - icon="address-card") + icon="address-card", + ) self.levels.append(ta) tech_ccs = models.TrainingLevel.objects.create( level=models.TrainingLevel.TECHNICIAN, description="Technician Common Competencies. Spirit abstract endless insofar horror sexuality depths war decrepit against strong aversion revaluation free. Christianity reason joy sea law mountains transvaluation. Sea battle aversion dead ultimate morality self. Faithful morality.", - icon="book-reader") + icon="book-reader", + ) tech_ccs.prerequisite_levels.add(ta) - super_ccs = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, description="Depths disgust hope faith of against hatred will victorious. Law...", icon="user-graduate") + super_ccs = models.TrainingLevel.objects.create( + level=models.TrainingLevel.SUPERVISOR, + description="Depths disgust hope faith of against hatred will victorious. Law...", + icon="user-graduate", + ) for i in range(0, 5): if len(items) == 0: break item = random.choice(items) items.remove(item) if i % 3 == 0: - models.TrainingLevelRequirement.objects.create(level=tech_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0]) + models.TrainingLevelRequirement.objects.create( + level=tech_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0] + ) else: - models.TrainingLevelRequirement.objects.create(level=super_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0]) + models.TrainingLevelRequirement.objects.create( + level=super_ccs, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0] + ) icons = { - models.TrainingLevel.SOUND: ('microphone', 'microphone-alt'), - models.TrainingLevel.LIGHTING: ('lightbulb', 'traffic-light'), - models.TrainingLevel.POWER: ('plug', 'bolt'), - models.TrainingLevel.RIGGING: ('link', 'pallet'), - models.TrainingLevel.HAULAGE: ('truck', 'route'), + models.TrainingLevel.SOUND: ("microphone", "microphone-alt"), + models.TrainingLevel.LIGHTING: ("lightbulb", "traffic-light"), + models.TrainingLevel.POWER: ("plug", "bolt"), + models.TrainingLevel.RIGGING: ("link", "pallet"), + models.TrainingLevel.HAULAGE: ("truck", "route"), } for i, name in models.TrainingLevel.DEPARTMENTS: - technician = models.TrainingLevel.objects.create(level=models.TrainingLevel.TECHNICIAN, department=i, description="Moral pinnacle derive ultimate war dead. Strong fearful joy contradict battle christian faithful enlightenment prejudice zarathustra moral.", icon=icons[i][0]) + technician = models.TrainingLevel.objects.create( + level=models.TrainingLevel.TECHNICIAN, + department=i, + description="Moral pinnacle derive ultimate war dead. Strong fearful joy contradict battle christian faithful enlightenment prejudice zarathustra moral.", + icon=icons[i][0], + ) technician.prerequisite_levels.add(tech_ccs) - supervisor = models.TrainingLevel.objects.create(level=models.TrainingLevel.SUPERVISOR, department=i, description="Spirit holiest merciful mountains inexpedient reason value. Suicide ultimate hope.", icon=icons[i][1]) + supervisor = models.TrainingLevel.objects.create( + level=models.TrainingLevel.SUPERVISOR, + department=i, + description="Spirit holiest merciful mountains inexpedient reason value. Suicide ultimate hope.", + icon=icons[i][1], + ) supervisor.prerequisite_levels.add(super_ccs, technician) for i in range(0, 30): @@ -204,9 +225,17 @@ class Command(BaseCommand): items.remove(item) try: if i % 3 == 0: - models.TrainingLevelRequirement.objects.create(level=technician, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0]) + models.TrainingLevelRequirement.objects.create( + level=technician, + item=item, + depth=random.choice(models.TrainingItemQualification.CHOICES)[0], + ) else: - models.TrainingLevelRequirement.objects.create(level=supervisor, item=item, depth=random.choice(models.TrainingItemQualification.CHOICES)[0]) + models.TrainingLevelRequirement.objects.create( + level=supervisor, + item=item, + depth=random.choice(models.TrainingItemQualification.CHOICES)[0], + ) except: # noqa print("Failed create for {}. Weird.".format(item)) diff --git a/training/management/commands/generate_sample_training_users.py b/training/management/commands/generate_sample_training_users.py index 3d90a2ac..287d4e3e 100644 --- a/training/management/commands/generate_sample_training_users.py +++ b/training/management/commands/generate_sample_training_users.py @@ -1,18 +1,16 @@ -import datetime import random from django.contrib.auth.models import Group, Permission from django.core.management.base import BaseCommand, CommandError from django.db import transaction from django.utils import timezone -from reversion import revisions as reversion from training import models from RIGS.models import Profile class Command(BaseCommand): - help = 'Adds training users' + help = "Adds training users" can_import_settings = True profiles = [] @@ -23,9 +21,9 @@ class Command(BaseCommand): from django.conf import settings if not (settings.DEBUG or settings.STAGING): - raise CommandError('You cannot run this command in production') + raise CommandError("You cannot run this command in production") - random.seed('otherwise it is done by time, which could lead to inconsistent tests') + random.seed("otherwise it is done by time, which could lead to inconsistent tests") with transaction.atomic(): self.setup_groups() @@ -33,7 +31,7 @@ class Command(BaseCommand): print("Done generating useful training users") def setup_groups(self): - self.committee_group = Group.objects.create(name='Committee') + self.committee_group = Group.objects.create(name="Committee") perms = [ "add_trainingitemqualification", @@ -44,7 +42,8 @@ class Command(BaseCommand): "delete_traininglevelqualification", "add_traininglevelrequirement", "change_traininglevelrequirement", - "delete_traininglevelrequirement"] + "delete_traininglevelrequirement", + ] for permId in perms: self.committee_group.permissions.add(Permission.objects.get(codename=permId)) @@ -52,26 +51,40 @@ class Command(BaseCommand): self.committee_group.save() def setup_useful_profiles(self): - supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor", - initials="SV", - email="supervisor@example.com", is_active=True, - is_staff=True, is_approved=True, is_supervisor=True) - supervisor.set_password('supervisor') + supervisor = Profile.objects.create( + username="supervisor", + first_name="Super", + last_name="Visor", + initials="SV", + email="supervisor@example.com", + is_active=True, + is_staff=True, + is_approved=True, + is_supervisor=True, + ) + supervisor.set_password("supervisor") supervisor.groups.add(Group.objects.get(name="Keyholders")) supervisor.save() models.TrainingLevelQualification.objects.create( trainee=supervisor, - level=models.TrainingLevel.objects.filter( - level__gte=models.TrainingLevel.SUPERVISOR).exclude( - department=models.TrainingLevel.HAULAGE).exclude( - department__isnull=True).first(), + level=models.TrainingLevel.objects.filter(level__gte=models.TrainingLevel.SUPERVISOR) + .exclude(department=models.TrainingLevel.HAULAGE) + .exclude(department__isnull=True) + .first(), confirmed_on=timezone.now(), - confirmed_by=models.Trainee.objects.first()) + confirmed_by=models.Trainee.objects.first(), + ) - committee_user = Profile.objects.create(username="committee", first_name="Committee", last_name="Member", - initials="CM", - email="committee@example.com", is_active=True, is_approved=True) + committee_user = Profile.objects.create( + username="committee", + first_name="Committee", + last_name="Member", + initials="CM", + email="committee@example.com", + is_active=True, + is_approved=True, + ) committee_user.groups.add(self.committee_group) supervisor.groups.add(Group.objects.get(name="Keyholders")) - committee_user.set_password('committee') + committee_user.set_password("committee") committee_user.save() diff --git a/training/management/commands/import_old_db.py b/training/management/commands/import_old_db.py index 4896b555..ef60c43c 100644 --- a/training/management/commands/import_old_db.py +++ b/training/management/commands/import_old_db.py @@ -26,7 +26,7 @@ class Command(BaseCommand): @staticmethod def xml_path(file): - return os.path.join(settings.BASE_DIR, 'data/{}'.format(file)) + return os.path.join(settings.BASE_DIR, "data/{}".format(file)) @staticmethod def parse_xml(file): @@ -37,45 +37,46 @@ class Command(BaseCommand): def import_Trainees(self): tally = [0, 0] - root = self.parse_xml(self.xml_path('Members.xml')) + root = self.parse_xml(self.xml_path("Members.xml")) for child in root: try: - name = child.find('Member_x0020_Name').text + name = child.find("Member_x0020_Name").text first_name = name.split()[0] last_name = " ".join(name.split()[1:]) profile = Profile.objects.filter(first_name=first_name, last_name=last_name).first() if profile: - self.id_map[child.find('ID').text] = profile.pk + self.id_map[child.find("ID").text] = profile.pk print(f"Found existing user {profile}, matching data") tally[0] += 1 else: # PYTHONIC, BABY - initials = first_name[0] + "".join([name_section[0] for name_section in re.split("\\s*-", last_name.replace("(", ""))]) + initials = first_name[0] + "".join( + [name_section[0] for name_section in re.split("\\s*-", last_name.replace("(", ""))] + ) # print(initials) - new_profile = Profile.objects.create(username=name.replace(" ", ""), - first_name=first_name, - last_name=last_name, - initials=initials) - self.id_map[child.find('ID').text] = new_profile.pk + new_profile = Profile.objects.create( + username=name.replace(" ", ""), first_name=first_name, last_name=last_name, initials=initials + ) + self.id_map[child.find("ID").text] = new_profile.pk tally[1] += 1 print(f"No match found, creating new user {new_profile}") except AttributeError: # W.T.F - print("Trainee #{} is FUBAR".format(child.find('ID').text)) + print("Trainee #{} is FUBAR".format(child.find("ID").text)) - print('Trainees - Updated: {}, Created: {}'.format(tally[0], tally[1])) + print("Trainees - Updated: {}, Created: {}".format(tally[0], tally[1])) def import_TrainingCatagory(self): tally = [0, 0] - root = self.parse_xml(self.xml_path('Categories.xml')) + root = self.parse_xml(self.xml_path("Categories.xml")) for child in root: obj, created = models.TrainingCategory.objects.update_or_create( - pk=int(child.find('ID').text), - reference_number=int(child.find('Category_x0020_Number').text), - name=child.find('Category_x0020_Name').text + pk=int(child.find("ID").text), + reference_number=int(child.find("Category_x0020_Number").text), + name=child.find("Category_x0020_Name").text, ) if created: @@ -83,30 +84,30 @@ class Command(BaseCommand): else: tally[0] += 1 - print('Categories - Updated: {}, Created: {}'.format(tally[0], tally[1])) + print("Categories - Updated: {}, Created: {}".format(tally[0], tally[1])) def import_TrainingItem(self): tally = [0, 0] - root = self.parse_xml(self.xml_path('Training Items.xml')) + root = self.parse_xml(self.xml_path("Training Items.xml")) for child in root: - if child.find('active').text == '0': + if child.find("active").text == "0": active = False else: active = True - number = int(child.find('Item_x0020_Number').text) - name = child.find('Item_x0020_Name').text - category = models.TrainingCategory.objects.get(pk=int(child.find('Category_x0020_ID').text)) + number = int(child.find("Item_x0020_Number").text) + name = child.find("Item_x0020_Name").text + category = models.TrainingCategory.objects.get(pk=int(child.find("Category_x0020_ID").text)) try: obj, created = models.TrainingItem.objects.update_or_create( - pk=int(child.find('ID').text), + pk=int(child.find("ID").text), reference_number=number, description=name, category=category, - active=active + active=active, ) except IntegrityError: print(f"Training Item {category.reference_number}.{number} {name} has a duplicate reference number") @@ -116,38 +117,46 @@ class Command(BaseCommand): else: tally[0] += 1 - print(f'Training Items - Updated: {tally[0]}, Created: {tally[1]}') + print(f"Training Items - Updated: {tally[0]}, Created: {tally[1]}") def import_TrainingItemQualification(self): tally = [0, 0, 0] - root = self.parse_xml(self.xml_path('Training Records.xml')) + root = self.parse_xml(self.xml_path("Training Records.xml")) for child in root: - depths = [("Training_Started", models.TrainingItemQualification.STARTED), - ("Training_Complete", models.TrainingItemQualification.COMPLETE), - ("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), ] + depths = [ + ("Training_Started", models.TrainingItemQualification.STARTED), + ("Training_Complete", models.TrainingItemQualification.COMPLETE), + ("Competency_Assessed", models.TrainingItemQualification.PASSED_OUT), + ] - for (depth, depth_index) in depths: - if child.find(f'{depth}_Date') is not None: - if child.find(f'{depth}_Assessor_ID') is None: + for depth, depth_index in depths: + if child.find(f"{depth}_Date") is not None: + if child.find(f"{depth}_Assessor_ID") is None: print(f"Training Record #{child.find('ID').text} had no supervisor. Assigning System User.") supervisor = Profile.objects.get(first_name="God") continue - supervisor = Profile.objects.get(pk=self.id_map[child.find('{}_Assessor_ID'.format(depth)).text]) - if child.find('Member_ID') is None: - print("Training Record #{} didn't train anybody and has been ignored. Dammit {}".format(child.find('ID').text, supervisor.name)) + supervisor = Profile.objects.get(pk=self.id_map[child.find("{}_Assessor_ID".format(depth)).text]) + if child.find("Member_ID") is None: + print( + "Training Record #{} didn't train anybody and has been ignored. Dammit {}".format( + child.find("ID").text, supervisor.name + ) + ) tally[2] += 1 continue try: obj, created = models.TrainingItemQualification.objects.update_or_create( - item=models.TrainingItem.objects.get(pk=int(child.find('Training_Item_ID').text)), - trainee=Profile.objects.get(pk=self.id_map[child.find('Member_ID').text]), + item=models.TrainingItem.objects.get(pk=int(child.find("Training_Item_ID").text)), + trainee=Profile.objects.get(pk=self.id_map[child.find("Member_ID").text]), depth=depth_index, - date=child.find('{}_Date'.format(depth)).text[:-9], # Stored as datetime with time as midnight because fuck you I guess - supervisor=supervisor + date=child.find("{}_Date".format(depth)).text[ + :-9 + ], # Stored as datetime with time as midnight because fuck you I guess + supervisor=supervisor, ) - notes = child.find('{}_Notes'.format(depth)) + notes = child.find("{}_Notes".format(depth)) if notes is not None: obj.notes = notes.text obj.save() @@ -156,19 +165,21 @@ class Command(BaseCommand): else: tally[0] += 1 except IntegrityError: # Eh? - print("Training Record #{} is probably duplicate. ಠ_ಠ".format(child.find('ID').text)) + print("Training Record #{} is probably duplicate. ಠ_ಠ".format(child.find("ID").text)) except AttributeError: - print(child.find('ID').text) + print(child.find("ID").text) - print('Training Item Qualifications - Updated: {}, Created: {}, Broken: {}'.format(tally[0], tally[1], tally[2])) + print( + "Training Item Qualifications - Updated: {}, Created: {}, Broken: {}".format(tally[0], tally[1], tally[2]) + ) def import_TrainingLevel(self): tally = [0, 0] - root = self.parse_xml(self.xml_path('Training Levels.xml')) + root = self.parse_xml(self.xml_path("Training Levels.xml")) for child in root: - name = child.find('Level_x0020_Name').text + name = child.find("Level_x0020_Name").text if name == "Technical Assistant": level = models.TrainingLevel.TA depString = None @@ -194,13 +205,11 @@ class Command(BaseCommand): department = dep[0] desc = "" - if child.find('Desc') is not None: - desc = child.find('Desc').text + if child.find("Desc") is not None: + desc = child.find("Desc").text obj, created = models.TrainingLevel.objects.update_or_create( - pk=int(child.find('ID').text), - description=desc, - level=level + pk=int(child.find("ID").text), description=desc, level=level ) if depString is not None: obj.department = department @@ -214,32 +223,56 @@ class Command(BaseCommand): for level in models.TrainingLevel.objects.all(): if level.department is not None: if level.level == models.TrainingLevel.TECHNICIAN: - level.prerequisite_levels.add(models.TrainingLevel.objects.get(level=models.TrainingLevel.TA), models.TrainingLevel.objects.get(level=models.TrainingLevel.TECHNICIAN, department=None)) + level.prerequisite_levels.add( + models.TrainingLevel.objects.get(level=models.TrainingLevel.TA), + models.TrainingLevel.objects.get(level=models.TrainingLevel.TECHNICIAN, department=None), + ) elif level.level == models.TrainingLevel.SUPERVISOR: - level.prerequisite_levels.add(models.TrainingLevel.objects.get(level=models.TrainingLevel.TECHNICIAN, department=level.department), models.TrainingLevel.objects.get(level=models.TrainingLevel.SUPERVISOR, department=None)) + level.prerequisite_levels.add( + models.TrainingLevel.objects.get( + level=models.TrainingLevel.TECHNICIAN, department=level.department + ), + models.TrainingLevel.objects.get(level=models.TrainingLevel.SUPERVISOR, department=None), + ) - print('Training Levels - Updated: {}, Created: {}'.format(tally[0], tally[1])) + print("Training Levels - Updated: {}, Created: {}".format(tally[0], tally[1])) def import_TrainingLevelQualification(self): tally = [0, 0] - root = self.parse_xml(self.xml_path('Training Level Records.xml')) + root = self.parse_xml(self.xml_path("Training Level Records.xml")) for child in root: try: - trainee = Profile.objects.get(pk=self.id_map[child.find('Member_x0020_ID').text]) if child.find('Member_x0020_ID') is not None else False - level = models.TrainingLevel.objects.get(pk=int(child.find('Training_x0020_Level_x0020_ID').text)) if child.find('Training_x0020_Level_x0020_ID') is not None else False + trainee = ( + Profile.objects.get(pk=self.id_map[child.find("Member_x0020_ID").text]) + if child.find("Member_x0020_ID") is not None + else False + ) + level = ( + models.TrainingLevel.objects.get(pk=int(child.find("Training_x0020_Level_x0020_ID").text)) + if child.find("Training_x0020_Level_x0020_ID") is not None + else False + ) if trainee and level: - obj, created = models.TrainingLevelQualification.objects.update_or_create(pk=int(child.find('ID').text), - trainee=trainee, - level=level) + obj, created = models.TrainingLevelQualification.objects.update_or_create( + pk=int(child.find("ID").text), trainee=trainee, level=level + ) else: - print('Training Level Qualification #{} failed to import. Trainee: {} and Level: {}'.format(child.find('ID').text, trainee, level)) + print( + "Training Level Qualification #{} failed to import. Trainee: {} and Level: {}".format( + child.find("ID").text, trainee, level + ) + ) continue - if child.find('Date_x0020_Level_x0020_Awarded') is not None: - obj.confirmed_on = make_aware(datetime.datetime.strptime(child.find('Date_x0020_Level_x0020_Awarded').text.split('T')[0], "%Y-%m-%d")) + if child.find("Date_x0020_Level_x0020_Awarded") is not None: + obj.confirmed_on = make_aware( + datetime.datetime.strptime( + child.find("Date_x0020_Level_x0020_Awarded").text.split("T")[0], "%Y-%m-%d" + ) + ) obj.save() # confirmed by? @@ -248,27 +281,29 @@ class Command(BaseCommand): else: tally[0] += 1 except IntegrityError: # Eh? - print("Training Level Qualification #{} is duplicate. ಠ_ಠ".format(child.find('ID').text)) + print("Training Level Qualification #{} is duplicate. ಠ_ಠ".format(child.find("ID").text)) - print('TrainingLevelQualifications - Updated: {}, Created: {}'.format(tally[0], tally[1])) + print("TrainingLevelQualifications - Updated: {}, Created: {}".format(tally[0], tally[1])) def import_TrainingLevelRequirements(self): tally = [0, 0] - root = self.parse_xml(self.xml_path('Training Level Requirements.xml')) + root = self.parse_xml(self.xml_path("Training Level Requirements.xml")) for child in root: - items = child.find('Items').text.split(",") + items = child.find("Items").text.split(",") for item in items: try: - item = item.split('.') + item = item.split(".") obj, created = models.TrainingLevelRequirement.objects.update_or_create( - level=models.TrainingLevel.objects.get( - pk=int( - child.find('Level').text)), item=models.TrainingItem.objects.get( - active=True, reference_number=item[1], category=models.TrainingCategory.objects.get( - reference_number=item[0])), depth=int( - child.find('Depth').text)) + level=models.TrainingLevel.objects.get(pk=int(child.find("Level").text)), + item=models.TrainingItem.objects.get( + active=True, + reference_number=item[1], + category=models.TrainingCategory.objects.get(reference_number=item[0]), + ), + depth=int(child.find("Depth").text), + ) if created: tally[1] += 1 @@ -277,6 +312,11 @@ class Command(BaseCommand): except models.TrainingItem.DoesNotExist: print("Item with number {} does not exist".format(item)) except models.TrainingItem.MultipleObjectsReturned: - print(models.TrainingItem.objects.filter(reference_number=item[1], category=models.TrainingCategory.objects.get(reference_number=item[0]))) + print( + models.TrainingItem.objects.filter( + reference_number=item[1], + category=models.TrainingCategory.objects.get(reference_number=item[0]), + ) + ) - print('TrainingLevelRequirements - Updated: {}, Created: {}'.format(tally[0], tally[1])) + print("TrainingLevelRequirements - Updated: {}, Created: {}".format(tally[0], tally[1])) diff --git a/training/migrations/0008_unique_constraints.py b/training/migrations/0008_unique_constraints.py new file mode 100644 index 00000000..8cbc0816 --- /dev/null +++ b/training/migrations/0008_unique_constraints.py @@ -0,0 +1,45 @@ +# Generated by Django 5.2.17 on 2026-10-01 22:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('training', '0007_trainingitem_description'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='trainingitem', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='trainingitemqualification', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='traininglevelqualification', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='traininglevelrequirement', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='trainingitem', + constraint=models.UniqueConstraint(fields=('reference_number', 'active', 'category'), name='unique_reference_number_active_category'), + ), + migrations.AddConstraint( + model_name='trainingitemqualification', + constraint=models.UniqueConstraint(fields=('trainee', 'item', 'depth'), name='unique_trainee_item_depth'), + ), + migrations.AddConstraint( + model_name='traininglevelqualification', + constraint=models.UniqueConstraint(fields=('trainee', 'level'), name='unique_trainee_level'), + ), + migrations.AddConstraint( + model_name='traininglevelrequirement', + constraint=models.UniqueConstraint(fields=('level', 'item'), name='unique_level_item'), + ), + ] diff --git a/training/models.py b/training/models.py index c107890c..cc5a0d42 100644 --- a/training/models.py +++ b/training/models.py @@ -1,16 +1,13 @@ -import datetime from RIGS.models import Profile, filter_by_pk from reversion import revisions as reversion from django.core.exceptions import ValidationError from django.db import models -from django.db.models import Q, F, Value, CharField -from django.db.models.functions import Concat +from django.db.models import Q from django.urls import reverse from django.utils.safestring import mark_safe from versioning.versioning import RevisionMixin from queryable_properties.properties import queryable_property from queryable_properties.managers import QueryablePropertiesManager -from django.utils.translation import gettext_lazy as _ class TraineeManager(models.Manager): @@ -20,15 +17,13 @@ class TraineeManager(models.Manager): def search(self, query=None): qs = self.get_queryset() if query is not None: - or_lookup = (Q(first_name__icontains=query) | - Q(last_name__icontains=query) | Q(initials__icontains=query) - ) + or_lookup = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(initials__icontains=query) or_lookup = filter_by_pk(or_lookup, query) qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups return qs -@reversion.register(for_concrete_model=False, fields=['is_supervisor']) +@reversion.register(for_concrete_model=False, fields=["is_supervisor"]) class Trainee(Profile, RevisionMixin): class Meta: proxy = True @@ -37,31 +32,43 @@ class Trainee(Profile, RevisionMixin): # FIXME use queryset def started_levels(self): - return [level for level in TrainingLevel.objects.all() if level.percentage_complete(self) > 0 and level.pk not in self.level_qualifications.values_list('level', flat=True)] + return [ + level + for level in TrainingLevel.objects.all() + if level.percentage_complete(self) > 0 + and level.pk not in self.level_qualifications.values_list("level", flat=True) + ] @property def confirmed_levels(self): - return self.level_qualifications.exclude(confirmed_on=None).select_related('level') + return self.level_qualifications.exclude(confirmed_on=None).select_related("level") @property def is_technician(self): - return self.confirmed_levels \ - .filter(level__level=TrainingLevel.TECHNICIAN) \ - .exclude(level__department=TrainingLevel.HAULAGE) \ - .exclude(level__department__isnull=True).exists() + return ( + self.confirmed_levels.filter(level__level=TrainingLevel.TECHNICIAN) + .exclude(level__department=TrainingLevel.HAULAGE) + .exclude(level__department__isnull=True) + .exists() + ) @property def is_driver(self): return self.confirmed_levels.filter(level__department=TrainingLevel.HAULAGE).exists() def get_records_of_depth(self, depth): - return self.qualifications_obtained.filter(depth=depth).select_related('item', 'trainee', 'supervisor') + return self.qualifications_obtained.filter(depth=depth).select_related("item", "trainee", "supervisor") def is_user_qualified_in(self, item, required_depth): - return self.qualifications_obtained.values('item', 'depth').filter(item=item).filter(depth__gte=required_depth).exists() + return ( + self.qualifications_obtained.values("item", "depth") + .filter(item=item) + .filter(depth__gte=required_depth) + .exists() + ) def get_absolute_url(self): - return reverse('trainee_detail', kwargs={'pk': self.pk}) + return reverse("trainee_detail", kwargs={"pk": self.pk}) @property def display_id(self): @@ -71,21 +78,26 @@ class Trainee(Profile, RevisionMixin): class TrainingCategory(models.Model): reference_number = models.IntegerField(unique=True) name = models.CharField(max_length=50) - training_level = models.ForeignKey('TrainingLevel', on_delete=models.CASCADE, null=True, help_text="If this is set, any user with the selected level may pass out users within this category, regardless of other status") + training_level = models.ForeignKey( + "TrainingLevel", + on_delete=models.CASCADE, + null=True, + help_text="If this is set, any user with the selected level may pass out users within this category, regardless of other status", + ) def __str__(self): return f"{self.reference_number}. {self.name}" class Meta: - verbose_name_plural = 'Training Categories' - ordering = ['reference_number'] + verbose_name_plural = "Training Categories" + ordering = ["reference_number"] class TrainingItemManager(QueryablePropertiesManager): def search(self, query=None): qs = self.get_queryset() if query is not None: - or_lookup = (Q(name__icontains=query) | Q(description__icontains=query) | Q(display_id=query)) + or_lookup = Q(name__icontains=query) | Q(description__icontains=query) | Q(display_id=query) qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups return qs @@ -93,11 +105,11 @@ class TrainingItemManager(QueryablePropertiesManager): @reversion.register class TrainingItem(models.Model): reference_number = models.IntegerField() - category = models.ForeignKey('TrainingCategory', related_name='items', on_delete=models.CASCADE) + category = models.ForeignKey("TrainingCategory", related_name="items", on_delete=models.CASCADE) name = models.CharField(max_length=50) description = models.TextField(blank=True) active = models.BooleanField(default=True) - prerequisites = models.ManyToManyField('self', symmetrical=False, blank=True) + prerequisites = models.ManyToManyField("self", symmetrical=False, blank=True) objects = TrainingItemManager() @@ -112,9 +124,9 @@ class TrainingItem(models.Model): @display_id.filter @classmethod def display_id(cls, lookup, value): - if '.' in str(value): + if "." in str(value): try: - category_number, number = value.split('.', 2) + category_number, number = value.split(".", 2) if category_number and number: return Q(category__reference_number=int(category_number), reference_number=int(number)) except ValueError: @@ -128,29 +140,41 @@ class TrainingItem(models.Model): return name def get_absolute_url(self): - return reverse('item_list') + return reverse("item_list") def has_prereqs(self): return self.prerequisites.all().exists() def user_has_requirements(self, user): # Always true if there are no prerequisites, otherwise get a set of prerequsite IDs and check if they are a subset of the set of qualification IDs - return not self.has_prereqs() or set(self.prerequisites.values_list('pk', flat=True)).issubset(set(user.qualifications_obtained.values_list('item', flat=True))) + return not self.has_prereqs() or set(self.prerequisites.values_list("pk", flat=True)).issubset( + set(user.qualifications_obtained.values_list("item", flat=True)) + ) @staticmethod def user_has_qualification(item, user, depth): - return user.qualifications_obtained.only('item', 'depth').filter(item=item, depth__gte=depth).exists() + return user.qualifications_obtained.only("item", "depth").filter(item=item, depth__gte=depth).exists() class Meta: - unique_together = ["reference_number", "active", "category"] - ordering = ['category__reference_number', 'reference_number'] + constraints = [ + models.UniqueConstraint( + fields=["reference_number", "active", "category"], name="unique_reference_number_active_category" + ) + ] + ordering = ["category__reference_number", "reference_number"] class TrainingItemQualificationManager(QueryablePropertiesManager): def search(self, query=None): - qs = self.get_queryset().select_related('item', 'supervisor', 'item__category') + qs = self.get_queryset().select_related("item", "supervisor", "item__category") if query is not None: - or_lookup = (Q(item__name__icontains=query) | Q(supervisor__first_name__icontains=query) | Q(supervisor__last_name__icontains=query) | Q(item__category__name__icontains=query) | Q(item__display_id=query)) + or_lookup = ( + Q(item__name__icontains=query) + | Q(supervisor__first_name__icontains=query) + | Q(supervisor__last_name__icontains=query) + | Q(item__category__name__icontains=query) + | Q(item__display_id=query) + ) try: or_lookup = Q(item__category__reference_number=int(query)) | or_lookup @@ -167,16 +191,16 @@ class TrainingItemQualification(models.Model, RevisionMixin): COMPLETE = 1 PASSED_OUT = 2 CHOICES = ( - (STARTED, 'Training Started'), - (COMPLETE, 'Training Complete'), - (PASSED_OUT, 'Passed Out'), + (STARTED, "Training Started"), + (COMPLETE, "Training Complete"), + (PASSED_OUT, "Passed Out"), ) - item = models.ForeignKey('TrainingItem', on_delete=models.CASCADE) + item = models.ForeignKey("TrainingItem", on_delete=models.CASCADE) depth = models.IntegerField(choices=CHOICES) - trainee = models.ForeignKey('Trainee', related_name='qualifications_obtained', on_delete=models.CASCADE) + trainee = models.ForeignKey("Trainee", related_name="qualifications_obtained", on_delete=models.CASCADE) date = models.DateField() # TODO Remember that some training is external. Support for making an organisation the trainer? - supervisor = models.ForeignKey('Trainee', related_name='qualifications_granted', on_delete=models.CASCADE) + supervisor = models.ForeignKey("Trainee", related_name="qualifications_granted", on_delete=models.CASCADE) notes = models.TextField(blank=True) # TODO Maximum depth - some things stop at Complete and you can't be passed out in them @@ -185,15 +209,17 @@ class TrainingItemQualification(models.Model, RevisionMixin): def clean(self): errdict = {} # Validate supervisor can train in this item - if hasattr(self, 'supervisor'): # This will be false if form validation fails + if hasattr(self, "supervisor"): # This will be false if form validation fails if self.item.category.training_level: if not self.supervisor.level_qualifications.filter(level=self.item.category.training_level): - errdict['supervisor'] = ('Selected supervising person is missing requisite training level to train in this department') + errdict["supervisor"] = ( + "Selected supervising person is missing requisite training level to train in this department" + ) elif not self.supervisor.is_supervisor: - errdict['supervisor'] = ('Selected supervisor must actually *be* a supervisor...') + errdict["supervisor"] = "Selected supervisor must actually *be* a supervisor..." # Item requirements only apply to being passed out if self.depth == TrainingItemQualification.PASSED_OUT and not self.item.user_has_requirements(self.trainee): - errdict['item'] = ('Missing prerequisites') + errdict["item"] = "Missing prerequisites" if errdict != {}: # If there was an error when validation raise ValidationError(errdict) @@ -214,11 +240,11 @@ class TrainingItemQualification(models.Model, RevisionMixin): return "info" def get_absolute_url(self): - return reverse('edit_qualification', kwargs={'pk': self.pk}) + return reverse("edit_qualification", kwargs={"pk": self.pk}) class Meta: - unique_together = ["trainee", "item", "depth"] - order_with_respect_to = 'item' + constraints = [models.UniqueConstraint(fields=["trainee", "item", "depth"], name="unique_trainee_item_depth")] + order_with_respect_to = "item" # Levels @@ -229,9 +255,9 @@ class TrainingLevel(models.Model, RevisionMixin): TECHNICIAN = 1 SUPERVISOR = 2 CHOICES = ( - (TA, 'Technical Assistant'), - (TECHNICIAN, 'Technician'), - (SUPERVISOR, 'Supervisor'), + (TA, "Technical Assistant"), + (TECHNICIAN, "Technician"), + (SUPERVISOR, "Supervisor"), ) SOUND = 0 LIGHTING = 1 @@ -239,15 +265,17 @@ class TrainingLevel(models.Model, RevisionMixin): RIGGING = 3 HAULAGE = 4 DEPARTMENTS = ( - (SOUND, 'Sound'), - (LIGHTING, 'Lighting'), - (POWER, 'Power'), - (RIGGING, 'Rigging'), - (HAULAGE, 'Haulage'), + (SOUND, "Sound"), + (LIGHTING, "Lighting"), + (POWER, "Power"), + (RIGGING, "Rigging"), + (HAULAGE, "Haulage"), ) - department = models.IntegerField(choices=DEPARTMENTS, null=True, blank=True) # N.B. Technical Assistant does not have a department + department = models.IntegerField( + choices=DEPARTMENTS, null=True, blank=True + ) # N.B. Technical Assistant does not have a department level = models.IntegerField(choices=CHOICES) - prerequisite_levels = models.ManyToManyField('self', related_name='prerequisites', symmetrical=False, blank=True) + prerequisite_levels = models.ManyToManyField("self", related_name="prerequisites", symmetrical=False, blank=True) icon = models.CharField(null=True, blank=True, max_length=20) class Meta: @@ -288,7 +316,7 @@ class TrainingLevel(models.Model, RevisionMixin): return self.get_requirements_of_depth(TrainingItemQualification.PASSED_OUT) def percentage_complete(self, user): - needed_qualifications = self.requirements.all().select_related('item') + needed_qualifications = self.requirements.all().select_related("item") relavant_qualifications = 0.0 # TODO Efficiency... for req in needed_qualifications: @@ -301,9 +329,13 @@ class TrainingLevel(models.Model, RevisionMixin): return 0 def user_has_requirements(self, user): - has_required_items = all(TrainingItem.user_has_qualification(req.item, user, req.depth) for req in self.requirements.all()) + has_required_items = all( + TrainingItem.user_has_qualification(req.item, user, req.depth) for req in self.requirements.all() + ) # Always true if there are no prerequisites, otherwise get a set of prerequsite IDs and check if they are a subset of the set of qualification IDs - has_required_levels = not self.prerequisite_levels.all().exists() or set(self.prerequisite_levels.values_list('pk', flat=True)).issubset(set(user.level_qualifications.values_list('level', flat=True))) + has_required_levels = not self.prerequisite_levels.all().exists() or set( + self.prerequisite_levels.values_list("pk", flat=True) + ).issubset(set(user.level_qualifications.values_list("level", flat=True))) return has_required_items and has_required_levels def __str__(self): @@ -320,7 +352,7 @@ class TrainingLevel(models.Model, RevisionMixin): return str(self) def get_absolute_url(self): - return reverse('level_detail', kwargs={'pk': self.pk}) + return reverse("level_detail", kwargs={"pk": self.pk}) @property def get_icon(self): @@ -328,13 +360,15 @@ class TrainingLevel(models.Model, RevisionMixin): icon = f"" else: icon = "".join([w[0] for w in str(self).split()]) - return mark_safe(f"{icon}") + return mark_safe( + f"{icon}" + ) @reversion.register class TrainingLevelRequirement(models.Model, RevisionMixin): - level = models.ForeignKey('TrainingLevel', related_name='requirements', on_delete=models.CASCADE) - item = models.ForeignKey('TrainingItem', on_delete=models.CASCADE) + level = models.ForeignKey("TrainingLevel", related_name="requirements", on_delete=models.CASCADE) + item = models.ForeignKey("TrainingItem", on_delete=models.CASCADE) depth = models.IntegerField(choices=TrainingItemQualification.CHOICES) reversion_hide = True @@ -344,15 +378,15 @@ class TrainingLevelRequirement(models.Model, RevisionMixin): return f"{depth} in {self.item}" class Meta: - unique_together = ["level", "item"] + constraints = [models.UniqueConstraint(fields=["level", "item"], name="unique_level_item")] @reversion.register class TrainingLevelQualification(models.Model, RevisionMixin): - trainee = models.ForeignKey('Trainee', related_name='level_qualifications', on_delete=models.CASCADE) - level = models.ForeignKey('TrainingLevel', on_delete=models.CASCADE) + trainee = models.ForeignKey("Trainee", related_name="level_qualifications", on_delete=models.CASCADE) + level = models.ForeignKey("TrainingLevel", on_delete=models.CASCADE) confirmed_on = models.DateTimeField(null=True) - confirmed_by = models.ForeignKey('Trainee', related_name='confirmer', on_delete=models.CASCADE, null=True) + confirmed_by = models.ForeignKey("Trainee", related_name="confirmer", on_delete=models.CASCADE, null=True) @property def get_icon(self): @@ -373,8 +407,8 @@ class TrainingLevelQualification(models.Model, RevisionMixin): return str(self) def get_absolute_url(self): - return reverse('trainee_detail', kwargs={'pk': self.trainee_id}) + return reverse("trainee_detail", kwargs={"pk": self.trainee_id}) class Meta: - unique_together = ["trainee", "level"] - ordering = ['-confirmed_on'] + constraints = [models.UniqueConstraint(fields=["trainee", "level"], name="unique_trainee_level")] + ordering = ["-confirmed_on"] diff --git a/training/templates/add_level_requirement.html b/training/templates/add_level_requirement.html index e38d45c8..58883d42 100644 --- a/training/templates/add_level_requirement.html +++ b/training/templates/add_level_requirement.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_training.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_training.html' %} {% load static %} {% load widget_tweaks %} @@ -37,7 +37,7 @@ {% render_field form.depth|add_class:'form-control col-sm'|attr:'required' %}
    - {% if not request.is_ajax %} + {% if not is_ajax %} {% endif %} diff --git a/training/templates/edit_training_record.html b/training/templates/edit_training_record.html index c8d9b051..42f895ea 100644 --- a/training/templates/edit_training_record.html +++ b/training/templates/edit_training_record.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:'base_ajax.html,base_training.html' %} +{% extends is_ajax|yesno:'base_ajax.html,base_training.html' %} {% load static %} {% load widget_tweaks %} @@ -51,7 +51,7 @@ {% render_field form.notes|add_class:'form-control' rows=3 %} - {% if not request.is_ajax %} + {% if not is_ajax %}
    {% button 'submit' %}
    diff --git a/training/templatetags/tags.py b/training/templatetags/tags.py index 30c18fbc..072a6056 100644 --- a/training/templatetags/tags.py +++ b/training/templatetags/tags.py @@ -1,8 +1,5 @@ -from django import forms from django import template -from django.utils.html import escape -from django.utils.safestring import SafeData, mark_safe -from django.utils.text import normalize_newlines +from django.utils.safestring import mark_safe from django.urls import reverse from training import models @@ -15,7 +12,9 @@ def user_has_qualification(user, item, depth): if models.TrainingItem.user_has_qualification(item, user, depth): return mark_safe("") else: - return mark_safe("") + return mark_safe( + "" + ) @register.simple_tag @@ -35,14 +34,23 @@ def colour_from_depth(depth): @register.filter def get_levels_of_depth(trainee, level): - return trainee.level_qualifications.all().exclude(confirmed_on=None).exclude(level__department=None).exclude(level__department=models.TrainingLevel.HAULAGE).select_related('level').filter(level__level=level) + return ( + trainee.level_qualifications.all() + .exclude(confirmed_on=None) + .exclude(level__department=None) + .exclude(level__department=models.TrainingLevel.HAULAGE) + .select_related("level") + .filter(level__level=level) + ) @register.simple_tag def confirm_button(user, trainee, level): if level.user_has_requirements(trainee): string = "Awaiting Confirmation" - if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm('training.add_traininglevelqualification'): + if models.Trainee.objects.get(pk=user.pk).is_supervisor or user.has_perm( + "training.add_traininglevelqualification" + ): string += f"Confirm" return mark_safe(string) else: diff --git a/training/tests/conftest.py b/training/tests/conftest.py index 08c58845..40c241ed 100644 --- a/training/tests/conftest.py +++ b/training/tests/conftest.py @@ -5,18 +5,31 @@ from RIGS.models import Profile @pytest.fixture def trainee(db): - trainee = Profile.objects.create(username="trainee", first_name="Train", last_name="EE", - initials="TRN", - email="trainee@example.com", is_active=True, is_approved=True) + trainee = Profile.objects.create( + username="trainee", + first_name="Train", + last_name="EE", + initials="TRN", + email="trainee@example.com", + is_active=True, + is_approved=True, + ) yield trainee trainee.delete() @pytest.fixture def supervisor(db): - supervisor = Profile.objects.create(username="supervisor", first_name="Super", last_name="Visor", - initials="SV", - email="supervisor@example.com", is_supervisor=True, is_active=True, is_approved=True) + supervisor = Profile.objects.create( + username="supervisor", + first_name="Super", + last_name="Visor", + initials="SV", + email="supervisor@example.com", + is_supervisor=True, + is_active=True, + is_approved=True, + ) yield supervisor supervisor.delete() @@ -24,7 +37,9 @@ def supervisor(db): @pytest.fixture def training_item(db): training_category = models.TrainingCategory.objects.create(reference_number=1, name="The Basics") - training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="How Not To Die") + training_item = models.TrainingItem.objects.create( + category=training_category, reference_number=1, description="How Not To Die" + ) yield training_item training_category.delete() training_item.delete() @@ -33,7 +48,9 @@ def training_item(db): @pytest.fixture def training_item_2(db): training_category = models.TrainingCategory.objects.create(reference_number=2, name="Sound") - training_item = models.TrainingItem.objects.create(category=training_category, reference_number=1, description="Fundamentals of Audio") + training_item = models.TrainingItem.objects.create( + category=training_category, reference_number=1, description="Fundamentals of Audio" + ) yield training_item training_category.delete() training_item.delete() @@ -41,6 +58,8 @@ def training_item_2(db): @pytest.fixture def level(db): - level = models.TrainingLevel.objects.create(description="There is no description.", level=models.TrainingLevel.TECHNICIAN) + level = models.TrainingLevel.objects.create( + description="There is no description.", level=models.TrainingLevel.TECHNICIAN + ) yield level level.delete() diff --git a/training/tests/pages.py b/training/tests/pages.py deleted file mode 100644 index c55c1516..00000000 --- a/training/tests/pages.py +++ /dev/null @@ -1,81 +0,0 @@ -from django.urls import reverse -from pypom import Region -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as EC - -from PyRIGS.tests import regions -from PyRIGS.tests.pages import BasePage, FormPage - - -class TraineeDetail(BasePage): - URL_TEMPLATE = 'training/trainee/{pk}' - - _name_selector = (By.XPATH, '//h2') - - @property - def page_name(self): - return self.find_element(*self._name_selector).text - - -class AddQualification(FormPage): - URL_TEMPLATE = 'training/trainee/{pk}/add_qualification/' - - _item_selector = (By.XPATH, '//div[1]/form/div[1]/div') - _supervisor_selector = (By.XPATH, '//div[1]/form/div[3]/div') - - form_items = { - 'depth': (regions.SingleSelectPicker, (By.ID, 'id_depth')), - 'date': (regions.DatePicker, (By.ID, 'id_date')), - 'notes': (regions.TextBox, (By.ID, 'id_notes')), - } - - @property - def item_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._item_selector)) - - @property - def supervisor_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector)) - - @property - def success(self): - return 'add' not in self.driver.current_url - - -class SessionLog(FormPage): - URL_TEMPLATE = 'training/session_log' - - _supervisor_selector = (By.CSS_SELECTOR, 'div#supervisor_group>div.bootstrap-select') - _trainees_selector = (By.CSS_SELECTOR, 'div#trainees_group>div.bootstrap-select') - _training_started_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div') - _training_complete_selector = (By.XPATH, '//div[1]/div/div/form/div[4]/div') - _competency_assessed_selector = (By.XPATH, '//div[1]/div/div/form/div[5]/div') - - form_items = { - 'date': (regions.DatePicker, (By.ID, 'id_date')), - 'notes': (regions.TextBox, (By.ID, 'id_notes')), - } - - @property - def supervisor_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._supervisor_selector)) - - @property - def trainees_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._trainees_selector)) - - @property - def training_started_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._training_started_selector)) - - @property - def training_complete_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._training_complete_selector)) - - @property - def competency_assessed_selector(self): - return regions.BootstrapSelectElement(self, self.find_element(*self._competency_assessed_selector)) - - @property - def success(self): - return 'log' not in self.driver.current_url diff --git a/training/tests/test_interaction.py b/training/tests/test_interaction.py deleted file mode 100644 index ce24cadc..00000000 --- a/training/tests/test_interaction.py +++ /dev/null @@ -1,80 +0,0 @@ -import datetime -import time - -from django.utils import timezone -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as ec -from selenium.webdriver.support.ui import WebDriverWait - -from PyRIGS.tests.base import AutoLoginTest, screenshot_failure_cls, assert_times_almost_equal -from PyRIGS.tests.pages import animation_is_finished -from training import models -from training.tests import pages - - -def select_super(page, supervisor): - page.supervisor_selector.toggle() - assert page.supervisor_selector.is_open - page.supervisor_selector.search(supervisor.name[:-6]) - time.sleep(2) # Slow down for javascript - page.supervisor_selector.set_option(supervisor.name, True) - assert page.supervisor_selector.options[0].selected - - -def test_add_qualification(logged_in_browser, live_server, trainee, supervisor, training_item): - page = pages.AddQualification(logged_in_browser.driver, live_server.url, pk=trainee.pk).open() - # assert page.name in str(trainee) - - page.depth = "Training Started" - page.date = date = datetime.date(1984, 1, 1) - page.notes = "A note" - - time.sleep(2) # Slow down for javascript - - page.item_selector.toggle() - assert page.item_selector.is_open - page.item_selector.search(training_item.description) - time.sleep(2) # Slow down for javascript - # page.item_selector.set_option(training_item.description, True) - assert page.item_selector.options[0].selected - page.item_selector.toggle() - - select_super(page, supervisor) - page.supervisor_selector.toggle() - - page.submit() - assert page.success - qualification = models.TrainingItemQualification.objects.get(trainee=trainee, item=training_item) - assert qualification.supervisor_id == supervisor.pk - assert qualification.date == date - assert qualification.notes == "A note" - assert qualification.depth == models.TrainingItemQualification.STARTED - - -def test_session_log(logged_in_browser, live_server, trainee, supervisor, training_item, training_item_2): - page = pages.SessionLog(logged_in_browser.driver, live_server.url).open() - - page.date = date = datetime.date(2001, 1, 10) - page.notes = note = "A general note" - - time.sleep(2) # Slow down for javascript - - select_super(page, supervisor) - - page.trainees_selector.toggle() - assert page.trainees_selector.is_open - page.trainees_selector.search(trainee.first_name) - time.sleep(2) # Slow down for javascript - page.trainees_selector.set_option(trainee.name, True) - # assert page.trainees_selector.options[0].selected - page.trainees_selector.toggle() - - page.training_started_selector.toggle() - assert page.training_started_selector.is_open - page.training_started_selector.search(training_item.description[:-2]) - time.sleep(2) # Slow down for javascript - # assert page.training_started_selector.options[0].selected - page.training_started_selector.toggle() - - page.submit() - assert page.success diff --git a/training/tests/test_unit.py b/training/tests/test_unit.py index ed3154ef..8bb916dd 100644 --- a/training/tests/test_unit.py +++ b/training/tests/test_unit.py @@ -1,30 +1,43 @@ import datetime -import pytest from django.utils import timezone from django.urls import reverse -from pytest_django.asserts import assertFormError, assertRedirects, assertContains, assertNotContains, assertURLEqual +from pytest_django.asserts import assertFormError, assertContains, assertNotContains from training import models from reversion.models import Version, Revision def test_add_qualification(admin_client, trainee, admin_user, training_item): - url = reverse('add_qualification', kwargs={'pk': trainee.pk}) + url = reverse("add_qualification", kwargs={"pk": trainee.pk}) date = (timezone.now() + datetime.timedelta(days=3)).strftime("%Y-%m-%d") - response = admin_client.post(url, {'date': date, 'trainee': trainee.pk, 'supervisor': trainee.pk, 'item': training_item.pk}) - assertFormError(response, 'form', 'date', 'Qualification date may not be in the future') - assertFormError(response, 'form', 'supervisor', 'One may not supervise oneself...') - response = admin_client.post(url, {'date': date, 'trainee': admin_user.pk, 'supervisor': trainee.pk, 'item': training_item.pk}) + response = admin_client.post( + url, {"date": date, "trainee": trainee.pk, "supervisor": trainee.pk, "item": training_item.pk} + ) + assertFormError(response.context["form"], "date", "Qualification date may not be in the future") + assertFormError(response.context["form"], "supervisor", "One may not supervise oneself...") + response = admin_client.post( + url, {"date": date, "trainee": admin_user.pk, "supervisor": trainee.pk, "item": training_item.pk} + ) print(response.content) - assertFormError(response, 'form', 'supervisor', 'Selected supervisor must actually *be* a supervisor...') + assertFormError(response.context["form"], "supervisor", "Selected supervisor must actually *be* a supervisor...") def test_add_qualification_reversion(admin_client, trainee, training_item, supervisor): - url = reverse('add_qualification', kwargs={'pk': trainee.pk}) + url = reverse("add_qualification", kwargs={"pk": trainee.pk}) date = (timezone.now() + datetime.timedelta(days=-3)).strftime("%Y-%m-%d") - response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 0, 'notes': ""}) + response = admin_client.post( + url, + { + "date": date, + "supervisor": supervisor.pk, + "trainee": trainee.pk, + "item": training_item.pk, + "depth": 0, + "notes": "", + }, + ) print(response.content) assert response.status_code == 302 qual = models.TrainingItemQualification.objects.last() @@ -32,13 +45,15 @@ def test_add_qualification_reversion(admin_client, trainee, training_item, super assert training_item.pk == qual.item_id # Ensure only one revision has been created assert Revision.objects.count() == 1 - response = admin_client.post(url, {'date': date, 'supervisor': supervisor.pk, 'trainee': trainee.pk, 'item': training_item.pk, 'depth': 1}) + response = admin_client.post( + url, {"date": date, "supervisor": supervisor.pk, "trainee": trainee.pk, "item": training_item.pk, "depth": 1} + ) assert Revision.objects.count() == 2 assert Version.objects.count() == 4 # Two item qualifications and the trainee twice def test_add_requirement(admin_client, level): - url = reverse('add_requirement', kwargs={'pk': level.pk}) + url = reverse("add_requirement", kwargs={"pk": level.pk}) response = admin_client.post(url) assertContains(response, level.pk) @@ -51,32 +66,32 @@ def get_response(admin_client, url, kwargs={}): def test_trainee_detail(admin_client, trainee, admin_user): - response = get_response(admin_client, 'trainee_detail', {'pk': admin_user.pk}) + response = get_response(admin_client, "trainee_detail", {"pk": admin_user.pk}) assertContains(response, "Your Training Record") assertContains(response, "No qualifications in any levels") - response = get_response(admin_client, 'trainee_detail', {'pk': trainee.pk}) + response = get_response(admin_client, "trainee_detail", {"pk": trainee.pk}) assertNotContains(response, "Your") assertContains(response, f"{trainee.get_full_name()}'s Training Record") def test_trainee_item_detail(admin_client, trainee): - response = get_response(admin_client, 'trainee_item_detail', {'pk': trainee.pk}) + response = get_response(admin_client, "trainee_item_detail", {"pk": trainee.pk}) assertContains(response, "Nothing found") def test_item_list(admin_client, training_item): - response = get_response(admin_client, 'item_list') + response = get_response(admin_client, "item_list") assertContains(response, str(training_item.category)) def test_trainee_list_search(admin_client, admin_user, trainee, supervisor): - response = get_response(admin_client, 'trainee_list') + response = get_response(admin_client, "trainee_list") assertContains(response, admin_user.get_full_name()) assertContains(response, trainee.get_full_name()) assertContains(response, supervisor.get_full_name()) - url = reverse('trainee_list') - response = admin_client.get(url, {'q': trainee.get_full_name()}) + url = reverse("trainee_list") + response = admin_client.get(url, {"q": trainee.get_full_name()}) assertContains(response, trainee.get_full_name()) assertNotContains(response, supervisor.get_full_name()) diff --git a/training/urls.py b/training/urls.py index 55ab7523..59943003 100644 --- a/training/urls.py +++ b/training/urls.py @@ -7,27 +7,45 @@ from training import views, models from versioning.views import VersionHistory urlpatterns = [ - path('items/', login_required(views.ItemList.as_view()), name='item_list'), - path('items/export/', login_required(views.ItemListExport.as_view()), name='item_list_export'), - path('item//qualified_users/', login_required(views.ItemQualifications.as_view()), name='item_qualification'), - - path('trainee/list/', login_required(views.TraineeList.as_view()), name='trainee_list'), - path('trainee//', login_required(views.TraineeDetail.as_view()), - name='trainee_detail'), - path('trainee//history', login_required(VersionHistory.as_view()), name='trainee_history', kwargs={'model': models.Trainee, 'app': 'training'}), # Not picked up automatically because proxy model (I think) - path('trainee//add_qualification/', is_supervisor()(views.AddQualification.as_view()), - name='add_qualification'), - path('trainee/edit_qualification//', is_supervisor()(views.EditQualification.as_view()), - name='edit_qualification'), - - path('levels/', login_required(views.LevelList.as_view()), name='level_list'), - path('level//', login_required(views.LevelDetail.as_view()), name='level_detail'), - path('level//user//', login_required(views.LevelDetail.as_view()), name='level_detail'), - path('level//add_requirement/', is_supervisor()(views.AddLevelRequirement.as_view()), name='add_requirement'), - path('level/remove_requirement//', is_supervisor()(views.RemoveRequirement.as_view()), name='remove_requirement'), - - path('trainee//level//confirm', is_supervisor()(views.ConfirmLevel.as_view()), name='confirm_level'), - path('trainee//item_record', login_required(views.TraineeItemDetail.as_view()), name='trainee_item_detail'), - - path('session_log', is_supervisor()(views.SessionLog.as_view()), name='session_log'), + path("items/", login_required(views.ItemList.as_view()), name="item_list"), + path("items/export/", login_required(views.ItemListExport.as_view()), name="item_list_export"), + path( + "item//qualified_users/", login_required(views.ItemQualifications.as_view()), name="item_qualification" + ), + path("trainee/list/", login_required(views.TraineeList.as_view()), name="trainee_list"), + path("trainee//", login_required(views.TraineeDetail.as_view()), name="trainee_detail"), + path( + "trainee//history", + login_required(VersionHistory.as_view()), + name="trainee_history", + kwargs={"model": models.Trainee, "app": "training"}, + ), # Not picked up automatically because proxy model (I think) + path( + "trainee//add_qualification/", + is_supervisor()(views.AddQualification.as_view()), + name="add_qualification", + ), + path( + "trainee/edit_qualification//", + is_supervisor()(views.EditQualification.as_view()), + name="edit_qualification", + ), + path("levels/", login_required(views.LevelList.as_view()), name="level_list"), + path("level//", login_required(views.LevelDetail.as_view()), name="level_detail"), + path("level//user//", login_required(views.LevelDetail.as_view()), name="level_detail"), + path( + "level//add_requirement/", is_supervisor()(views.AddLevelRequirement.as_view()), name="add_requirement" + ), + path( + "level/remove_requirement//", + is_supervisor()(views.RemoveRequirement.as_view()), + name="remove_requirement", + ), + path( + "trainee//level//confirm", + is_supervisor()(views.ConfirmLevel.as_view()), + name="confirm_level", + ), + path("trainee//item_record", login_required(views.TraineeItemDetail.as_view()), name="trainee_item_detail"), + path("session_log", is_supervisor()(views.SessionLog.as_view()), name="session_log"), ] diff --git a/training/views.py b/training/views.py index eb9c2870..d72b00e5 100644 --- a/training/views.py +++ b/training/views.py @@ -10,7 +10,6 @@ from django.db.utils import IntegrityError from PyRIGS.views import is_ajax, ModalURLMixin, get_related, PrintListView from training import models, forms from users import views -from reversion.views import RevisionMixin class ItemList(generic.ListView): @@ -26,14 +25,14 @@ class ItemList(generic.ListView): class ItemListExport(PrintListView): model = models.TrainingItem - template_name = 'item_list.xml' + template_name = "item_list.xml" def get_queryset(self): return self.model.objects.filter(active=True) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['filename'] = "TrainingItemList.pdf" + context["filename"] = "TrainingItemList.pdf" context["categories"] = models.TrainingCategory.objects.all() return context @@ -43,7 +42,7 @@ class TraineeDetail(views.ProfileDetail): model = models.Trainee def get_queryset(self): - return self.model.objects.prefetch_related('qualifications_obtained') + return self.model.objects.prefetch_related("qualifications_obtained") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -53,21 +52,26 @@ class TraineeDetail(views.ProfileDetail): context["page_title"] = f"{self.object.get_full_name()}'s Training Record" context["started_levels"] = self.object.started_levels() context["completed_levels"] = self.object.level_qualifications.all() - context["categories"] = models.TrainingCategory.objects.all().prefetch_related('items') + context["categories"] = models.TrainingCategory.objects.all().prefetch_related("items") return context class TraineeItemDetail(generic.ListView): model = models.TrainingItemQualification - template_name = 'trainee_item_list.html' + template_name = "trainee_item_list.html" def get_queryset(self): - return models.Trainee.objects.get(pk=self.kwargs['pk']).qualifications_obtained.search(self.request.GET.get('q')).order_by('item__category__reference_number', 'item__reference_number').select_related('item', 'trainee') + return ( + models.Trainee.objects.get(pk=self.kwargs["pk"]) + .qualifications_obtained.search(self.request.GET.get("q")) + .order_by("item__category__reference_number", "item__reference_number") + .select_related("item", "trainee") + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - trainee = models.Trainee.objects.get(pk=self.kwargs['pk']) - context["trainee"] = models.Trainee.objects.get(pk=self.kwargs['pk']) + trainee = models.Trainee.objects.get(pk=self.kwargs["pk"]) + context["trainee"] = models.Trainee.objects.get(pk=self.kwargs["pk"]) context["page_title"] = f"Detailed Training Record for {trainee}" return context @@ -79,8 +83,10 @@ class LevelDetail(generic.DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_title"] = f"Training Level {self.object} {self.object.get_icon}" - context["users_with"] = map(lambda qual: qual.trainee, models.TrainingLevelQualification.objects.filter(level=self.object)) - context["u"] = models.Trainee.objects.get(pk=self.kwargs['u']) if 'u' in self.kwargs else self.request.user + context["users_with"] = map( + lambda qual: qual.trainee, models.TrainingLevelQualification.objects.filter(level=self.object) + ) + context["u"] = models.Trainee.objects.get(pk=self.kwargs["u"]) if "u" in self.kwargs else self.request.user return context @@ -96,17 +102,25 @@ class LevelList(generic.ListView): class TraineeList(generic.ListView): model = models.Trainee - template_name = 'trainee_list.html' + template_name = "trainee_list.html" paginate_by = 25 def get_queryset(self): - objects = self.model.objects.search(self.request.GET.get('q')) + objects = self.model.objects.search(self.request.GET.get("q")) - if self.request.GET.get('is_supervisor', ''): + if self.request.GET.get("is_supervisor", ""): objects = objects.filter(is_supervisor=True) - return objects.annotate(num_qualifications=Count('qualifications_obtained', filter=Q(qualifications_obtained__depth=models.TrainingItemQualification.PASSED_OUT)) - ).order_by('-num_qualifications').prefetch_related('level_qualifications', 'qualifications_obtained', 'qualifications_obtained__item') + return ( + objects.annotate( + num_qualifications=Count( + "qualifications_obtained", + filter=Q(qualifications_obtained__depth=models.TrainingItemQualification.PASSED_OUT), + ) + ) + .order_by("-num_qualifications") + .prefetch_related("level_qualifications", "qualifications_obtained", "qualifications_obtained__item") + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -120,50 +134,50 @@ class AddQualification(generic.CreateView, ModalURLMixin): form_class = forms.AddQualificationForm def form_valid(self, form, *args, **kwargs): - reversion.add_to_revision(form.cleaned_data['trainee']) + reversion.add_to_revision(form.cleaned_data["trainee"]) return super().form_valid(form, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["depths"] = models.TrainingItemQualification.CHOICES if is_ajax(self.request): - context['override'] = "base_ajax.html" + context["override"] = "base_ajax.html" else: - context['override'] = 'base_training.html' - trainee = models.Trainee.objects.get(pk=self.kwargs['pk']) - context['page_title'] = f"Add Qualification for {trainee}" - get_related(context['form'], context) + context["override"] = "base_training.html" + trainee = models.Trainee.objects.get(pk=self.kwargs["pk"]) + context["page_title"] = f"Add Qualification for {trainee}" + get_related(context["form"], context) return context def get_success_url(self): - return self.get_close_url('add_qualification', 'trainee_detail') + return self.get_close_url("add_qualification", "trainee_detail") def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['pk'] = self.kwargs['pk'] + kwargs["pk"] = self.kwargs["pk"] return kwargs class EditQualification(generic.UpdateView, ModalURLMixin): - template_name = 'edit_training_record.html' + template_name = "edit_training_record.html" model = models.TrainingItemQualification form_class = forms.QualificationForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["depths"] = models.TrainingItemQualification.CHOICES - context['page_title'] = f"Edit Qualification {self.object} for {self.object.trainee}" - get_related(context['form'], context) + context["page_title"] = f"Edit Qualification {self.object} for {self.object.trainee}" + get_related(context["form"], context) return context @transaction.atomic() @reversion.create_revision() def form_valid(self, form, *args, **kwargs): - reversion.add_to_revision(form.cleaned_data['trainee']) + reversion.add_to_revision(form.cleaned_data["trainee"]) return super().form_valid(form, *args, **kwargs) def get_success_url(self): - return self.get_close_url('edit_qualification', 'trainee_item_detail') + return self.get_close_url("edit_qualification", "trainee_item_detail") class AddLevelRequirement(generic.CreateView, ModalURLMixin): @@ -173,29 +187,29 @@ class AddLevelRequirement(generic.CreateView, ModalURLMixin): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - level = models.TrainingLevel.objects.get(pk=self.kwargs['pk']) + level = models.TrainingLevel.objects.get(pk=self.kwargs["pk"]) context["page_title"] = f"Add Requirements to Training Level {level}" - get_related(context['form'], context) + get_related(context["form"], context) return context def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['pk'] = self.kwargs['pk'] + kwargs["pk"] = self.kwargs["pk"] return kwargs def get_success_url(self): - return self.get_close_url('level_detail', 'level_detail') + return self.get_close_url("level_detail", "level_detail") @transaction.atomic() @reversion.create_revision() def form_valid(self, form, *args, **kwargs): - reversion.add_to_revision(form.cleaned_data['level']) + reversion.add_to_revision(form.cleaned_data["level"]) return super().form_valid(form, *args, **kwargs) class RemoveRequirement(generic.DeleteView): model = models.TrainingLevelRequirement - template_name = 'traininglevelrequirement_confirm_delete.html' + template_name = "traininglevelrequirement_confirm_delete.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -203,7 +217,7 @@ class RemoveRequirement(generic.DeleteView): return context def get_success_url(self): - return self.request.POST.get('next') + return self.request.POST.get("next") @transaction.atomic() @reversion.create_revision() @@ -216,8 +230,10 @@ class ConfirmLevel(generic.RedirectView): @transaction.atomic() @reversion.create_revision() def get_redirect_url(self, *args, **kwargs): - trainee = models.Trainee.objects.get(pk=kwargs['pk']) - level_qualification, created = models.TrainingLevelQualification.objects.get_or_create(trainee=trainee, level=models.TrainingLevel.objects.get(pk=kwargs['level_pk'])) + trainee = models.Trainee.objects.get(pk=kwargs["pk"]) + level_qualification, created = models.TrainingLevelQualification.objects.get_or_create( + trainee=trainee, level=models.TrainingLevel.objects.get(pk=kwargs["level_pk"]) + ) if created: user = self.request.user @@ -227,21 +243,28 @@ class ConfirmLevel(generic.RedirectView): level_qualification.save() reversion.add_to_revision(trainee) - return reverse_lazy('trainee_detail', kwargs={'pk': kwargs['pk']}) + return reverse_lazy("trainee_detail", kwargs={"pk": kwargs["pk"]}) class SessionLog(generic.FormView): - template_name = 'session_log_form.html' + template_name = "session_log_form.html" form_class = forms.SessionLogForm - success_url = reverse_lazy('trainee_list') + success_url = reverse_lazy("trainee_list") def form_valid(self, form, *args, **kwargs): - for trainee in form.cleaned_data.get('trainees', []): + for trainee in form.cleaned_data.get("trainees", []): for depth in models.TrainingItemQualification.CHOICES: - for item in form.cleaned_data.get(f'items_{depth[0]}', []): + for item in form.cleaned_data.get(f"items_{depth[0]}", []): try: with transaction.atomic(): - models.TrainingItemQualification.objects.create(trainee=trainee, item=item, supervisor=form.cleaned_data.get('supervisor'), depth=depth[0], notes=form.cleaned_data.get('notes'), date=form.cleaned_data.get('date')) + models.TrainingItemQualification.objects.create( + trainee=trainee, + item=item, + supervisor=form.cleaned_data.get("supervisor"), + depth=depth[0], + notes=form.cleaned_data.get("notes"), + date=form.cleaned_data.get("date"), + ) reversion.add_to_revision(trainee) except IntegrityError: pass # There was an attempt to create a duplicate qualification, ignore it @@ -251,7 +274,7 @@ class SessionLog(generic.FormView): context = super().get_context_data(**kwargs) context["depths"] = models.TrainingItemQualification.CHOICES context["page_title"] = "Log Training Session" - get_related(context['form'], context) + get_related(context["form"], context) return context @@ -261,7 +284,11 @@ class ItemQualifications(generic.ListView): paginate_by = 40 def get_queryset(self): - return models.TrainingItemQualification.objects.filter(item=self.kwargs['pk']).order_by('-depth').select_related('trainee') + return ( + models.TrainingItemQualification.objects.filter(item=self.kwargs["pk"]) + .order_by("-depth") + .select_related("trainee") + ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/users/forms.py b/users/forms.py index f37000c1..8c91816b 100644 --- a/users/forms.py +++ b/users/forms.py @@ -1,7 +1,6 @@ from hcaptcha.fields import hCaptchaField from django import forms -from django.contrib.auth.forms import (AuthenticationForm, PasswordResetForm, - UserChangeForm, UserCreationForm) +from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, UserChangeForm, UserCreationForm from django.conf import settings from registration.forms import RegistrationFormUniqueEmail @@ -11,9 +10,10 @@ from RIGS import models class CaptchaField(hCaptchaField): def validate(self, value): # Skip validation if we're testing FIXME: Arona, y u so lazy - if settings.HCAPTCHA_SITEKEY != '10000000-ffff-ffff-ffff-000000000001': + if settings.HCAPTCHA_SITEKEY != "10000000-ffff-ffff-ffff-000000000001": super().validate(value) + # Registration @@ -22,15 +22,15 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail): class Meta: model = models.Profile - fields = ('username', 'email', 'first_name', 'last_name', 'initials') + fields = ("username", "email", "first_name", "last_name", "initials") def clean_initials(self): """ Validate that the supplied initials are unique. """ - if models.Profile.objects.filter(initials__iexact=self.cleaned_data['initials']): + if models.Profile.objects.filter(initials__iexact=self.cleaned_data["initials"]): raise forms.ValidationError("These initials are already in use. Please supply different initials.") - return self.cleaned_data['initials'] + return self.cleaned_data["initials"] class CheckApprovedForm(AuthenticationForm): @@ -39,14 +39,15 @@ class CheckApprovedForm(AuthenticationForm): return AuthenticationForm.confirm_login_allowed(self, user) else: raise forms.ValidationError( - "Your account hasn't been approved by an administrator yet. Please check back in a few minutes!") + "Your account hasn't been approved by an administrator yet. Please check back in a few minutes!" + ) # Embedded Login form - remove the autofocus class EmbeddedAuthenticationForm(CheckApprovedForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['username'].widget.attrs.pop('autofocus', None) + self.fields["username"].widget.attrs.pop("autofocus", None) class PasswordReset(PasswordResetForm): diff --git a/users/management/commands/generateSampleUserData.py b/users/management/commands/generateSampleUserData.py index 323c9e62..c13655cb 100644 --- a/users/management/commands/generateSampleUserData.py +++ b/users/management/commands/generateSampleUserData.py @@ -1,17 +1,14 @@ -import datetime import random from django.contrib.auth.models import Group, Permission from django.core.management.base import BaseCommand, CommandError from django.db import transaction -from django.utils import timezone -from reversion import revisions as reversion from RIGS import models class Command(BaseCommand): - help = 'Adds sample data to use for testing' + help = "Adds sample data to use for testing" can_import_settings = True profiles = [] @@ -24,10 +21,11 @@ class Command(BaseCommand): from django.conf import settings if not (settings.DEBUG or settings.STAGING): - raise CommandError('You cannot run this command in production') + raise CommandError("You cannot run this command in production") random.seed( - 'Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistent tests + "Some object to seed the random number generator" + ) # otherwise it is done by time, which could lead to inconsistent tests with transaction.atomic(): self.setup_groups() @@ -36,23 +34,53 @@ class Command(BaseCommand): print("Done generating sample user data") def setup_groups(self): - self.keyholder_group = Group.objects.create(name='Keyholders') - self.finance_group = Group.objects.create(name='Finance') - self.hs_group = Group.objects.create(name='H&S') + self.keyholder_group = Group.objects.create(name="Keyholders") + self.finance_group = Group.objects.create(name="Finance") + self.hs_group = Group.objects.create(name="H&S") - keyholder_perms = ["add_event", "change_event", "view_event", - "add_eventitem", "change_eventitem", "delete_eventitem", - "add_organisation", "change_organisation", "view_organisation", - "add_person", "change_person", "view_person", "view_profile", - "add_venue", "change_venue", "view_venue", - "add_asset", "change_asset", "delete_asset", - "view_asset", "view_supplier", "change_supplier", "asset_finance", - "add_supplier", "view_cabletype", "change_cabletype", - "add_cabletype", "view_eventchecklist", "change_eventchecklist", - "add_eventchecklist", "view_riskassessment", "change_riskassessment", - "add_riskassessment"] - finance_perms = keyholder_perms + ["add_invoice", "change_invoice", "view_invoice", - "add_payment", "change_payment", "delete_payment"] + keyholder_perms = [ + "add_event", + "change_event", + "view_event", + "add_eventitem", + "change_eventitem", + "delete_eventitem", + "add_organisation", + "change_organisation", + "view_organisation", + "add_person", + "change_person", + "view_person", + "view_profile", + "add_venue", + "change_venue", + "view_venue", + "add_asset", + "change_asset", + "delete_asset", + "view_asset", + "view_supplier", + "change_supplier", + "asset_finance", + "add_supplier", + "view_cabletype", + "change_cabletype", + "add_cabletype", + "view_eventchecklist", + "change_eventchecklist", + "add_eventchecklist", + "view_riskassessment", + "change_riskassessment", + "add_riskassessment", + ] + finance_perms = keyholder_perms + [ + "add_invoice", + "change_invoice", + "view_invoice", + "add_payment", + "change_payment", + "delete_payment", + ] hs_perms = keyholder_perms + ["review_riskassessment", "review_eventchecklist"] for permId in keyholder_perms: @@ -68,13 +96,25 @@ class Command(BaseCommand): self.hs_group.save() def setup_generic_profiles(self): - names = ["Clara Oswin Oswald", "Rory Williams", "Amy Pond", "River Song", "Martha Jones", "Donna Noble", - "Jack Harkness", "Mickey Smith", "Rose Tyler"] + names = [ + "Clara Oswin Oswald", + "Rory Williams", + "Amy Pond", + "River Song", + "Martha Jones", + "Donna Noble", + "Jack Harkness", + "Mickey Smith", + "Rose Tyler", + ] for i, name in enumerate(names): - new_profile = models.Profile.objects.create(username=name.replace(" ", ""), first_name=name.split(" ")[0], - last_name=name.split(" ")[-1], - email=name.replace(" ", "") + "@example.com", - initials="".join([j[0].upper() for j in name.split()])) + new_profile = models.Profile.objects.create( + username=name.replace(" ", ""), + first_name=name.split(" ")[0], + last_name=name.split(" ")[-1], + email=name.replace(" ", "") + "@example.com", + initials="".join([j[0].upper() for j in name.split()]), + ) if i % 2 == 0: new_profile.phone = "01234 567894" @@ -82,32 +122,65 @@ class Command(BaseCommand): self.profiles.append(new_profile) def setup_useful_profiles(self): - super_user = models.Profile.objects.create_superuser(username="superuser", - email="superuser@example.com", password="superuser", first_name="Super", last_name="User", - initials="SU", is_active=True) + super_user = models.Profile.objects.create_superuser( + username="superuser", + email="superuser@example.com", + password="superuser", + first_name="Super", + last_name="User", + initials="SU", + is_active=True, + ) super_user.save() - finance_user = models.Profile.objects.create_user(username="finance", - email="financeuser@example.com", password="finance", first_name="Finance", last_name="User", - initials="FU", is_active=True, is_approved=True) + finance_user = models.Profile.objects.create_user( + username="finance", + email="financeuser@example.com", + password="finance", + first_name="Finance", + last_name="User", + initials="FU", + is_active=True, + is_approved=True, + ) finance_user.groups.add(self.finance_group) finance_user.groups.add(self.keyholder_group) finance_user.save() - hs_user = models.Profile.objects.create_user(username="hs", - email="hsuser@example.com", password="hs", first_name="HS", last_name="User", - initials="HSU", is_active=True, is_approved=True) + hs_user = models.Profile.objects.create_user( + username="hs", + email="hsuser@example.com", + password="hs", + first_name="HS", + last_name="User", + initials="HSU", + is_active=True, + is_approved=True, + ) hs_user.groups.add(self.hs_group) hs_user.groups.add(self.keyholder_group) hs_user.save() - keyholder_user = models.Profile.objects.create_user(username="keyholder", - email="keyholderuser@example.com", password="keyholder", first_name="Keyholder", last_name="User", - initials="KU", is_active=True, - is_approved=True) + keyholder_user = models.Profile.objects.create_user( + username="keyholder", + email="keyholderuser@example.com", + password="keyholder", + first_name="Keyholder", + last_name="User", + initials="KU", + is_active=True, + is_approved=True, + ) keyholder_user.groups.add(self.keyholder_group) keyholder_user.save() - basic_user = models.Profile.objects.create_user(username="basic", - email="basicuser@example.com", password="basic", first_name="Basic", last_name="User", - initials="BU", is_active=True, is_approved=True) + models.Profile.objects.create_user( + username="basic", + email="basicuser@example.com", + password="basic", + first_name="Basic", + last_name="User", + initials="BU", + is_active=True, + is_approved=True, + ) diff --git a/users/management/commands/usercleanup.py b/users/management/commands/usercleanup.py index b70877e9..cc1bc790 100644 --- a/users/management/commands/usercleanup.py +++ b/users/management/commands/usercleanup.py @@ -1,5 +1,4 @@ -from datetime import datetime, timedelta -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.utils import timezone from RIGS.models import Profile @@ -8,7 +7,7 @@ from training.models import TrainingLevel # This is triggered nightly by Heroku Scheduler class Command(BaseCommand): - help = 'Performs perodic user maintenance tasks' + help = "Performs perodic user maintenance tasks" def handle(self, *args, **options): for person in Profile.objects.all(): @@ -18,9 +17,13 @@ class Command(BaseCommand): person.is_approved = False person.save() # Ensure everyone with a supervisor level has the flag correctly set in the database - if person.level_qualifications.exclude(confirmed_on=None).select_related('level') \ - .filter(level__level__gte=TrainingLevel.SUPERVISOR) \ - .exclude(level__department=TrainingLevel.HAULAGE) \ - .exclude(level__department__isnull=True).exists(): + if ( + person.level_qualifications.exclude(confirmed_on=None) + .select_related("level") + .filter(level__level__gte=TrainingLevel.SUPERVISOR) + .exclude(level__department=TrainingLevel.HAULAGE) + .exclude(level__department__isnull=True) + .exists() + ): person.is_supervisor = True person.save() diff --git a/users/regbackend.py b/users/regbackend.py index e2f15f14..005befde 100644 --- a/users/regbackend.py +++ b/users/regbackend.py @@ -5,9 +5,9 @@ from users.forms import ProfileRegistrationFormUniqueEmail def user_created(sender, user, request, **kwargs): form = ProfileRegistrationFormUniqueEmail(request.POST) - user.first_name = form.data['first_name'] - user.last_name = form.data['last_name'] - user.initials = form.data['initials'] + user.first_name = form.data["first_name"] + user.last_name = form.data["last_name"] + user.initials = form.data["initials"] # user.phone = form.data['phone'] user.save() diff --git a/users/templates/partials/navbar_user.html b/users/templates/partials/navbar_user.html index 8d03047c..4b387174 100644 --- a/users/templates/partials/navbar_user.html +++ b/users/templates/partials/navbar_user.html @@ -14,7 +14,12 @@ -
  • Logout
  • +
  • +
    + {% csrf_token %} + +
    +
  • {% else %} diff --git a/users/templates/profile_detail.html b/users/templates/profile_detail.html index 0d3c4a1c..731ded3e 100644 --- a/users/templates/profile_detail.html +++ b/users/templates/profile_detail.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %} +{% extends is_ajax|yesno:"base_ajax.html,base_rigs.html" %} {% load static %} {% load linkornone from filters %} @@ -41,7 +41,7 @@ {% endblock %} {% block content %} - {% if not request.is_ajax and object.pk == user.pk %} + {% if not is_ajax and object.pk == user.pk %}
    @@ -85,7 +85,7 @@
    - {% if not request.is_ajax and object.pk == user.pk %} + {% if not is_ajax and object.pk == user.pk %}
    Personal iCal Details
    diff --git a/users/tests/test_users.py b/users/tests/test_users.py index d187630f..5dd74090 100644 --- a/users/tests/test_users.py +++ b/users/tests/test_users.py @@ -1,4 +1,3 @@ -import os import re import time @@ -24,84 +23,83 @@ class UserRegistrationTest(LiveServerTestCase): @override_settings(DEBUG=True) def test_registration(self): # Navigate to the registration page - self.browser.get(self.live_server_url + '/user/register/') - title_text = self.browser.find_element(By.TAG_NAME, 'h3').text + self.browser.get(self.live_server_url + "/user/register/") + title_text = self.browser.find_element(By.TAG_NAME, "h3").text self.assertIn("User Registration", title_text) # Check the form invites correctly - username = self.browser.find_element(By.ID, 'id_username') - self.assertEqual(username.get_attribute('placeholder'), 'Username') - email = self.browser.find_element(By.ID, 'id_email') - self.assertEqual(email.get_attribute('placeholder'), 'E-mail') + username = self.browser.find_element(By.ID, "id_username") + self.assertEqual(username.get_attribute("placeholder"), "Username") + email = self.browser.find_element(By.ID, "id_email") + self.assertEqual(email.get_attribute("placeholder"), "E-mail") # If this is correct we don't need to test it later - self.assertEqual(email.get_attribute('type'), 'email') - password1 = self.browser.find_element(By.ID, 'id_password1') - self.assertEqual(password1.get_attribute('placeholder'), 'Password') - self.assertEqual(password1.get_attribute('type'), 'password') - password2 = self.browser.find_element(By.ID, 'id_password2') - self.assertEqual( - password2.get_attribute('placeholder'), 'Password confirmation') - self.assertEqual(password2.get_attribute('type'), 'password') - first_name = self.browser.find_element(By.ID, 'id_first_name') - self.assertEqual(first_name.get_attribute('placeholder'), 'First name') - last_name = self.browser.find_element(By.ID, 'id_last_name') - self.assertEqual(last_name.get_attribute('placeholder'), 'Last name') - initials = self.browser.find_element(By.ID, 'id_initials') - self.assertEqual(initials.get_attribute('placeholder'), 'Initials') + self.assertEqual(email.get_attribute("type"), "email") + password1 = self.browser.find_element(By.ID, "id_password1") + self.assertEqual(password1.get_attribute("placeholder"), "Password") + self.assertEqual(password1.get_attribute("type"), "password") + password2 = self.browser.find_element(By.ID, "id_password2") + self.assertEqual(password2.get_attribute("placeholder"), "Password confirmation") + self.assertEqual(password2.get_attribute("type"), "password") + first_name = self.browser.find_element(By.ID, "id_first_name") + self.assertEqual(first_name.get_attribute("placeholder"), "First name") + last_name = self.browser.find_element(By.ID, "id_last_name") + self.assertEqual(last_name.get_attribute("placeholder"), "Last name") + initials = self.browser.find_element(By.ID, "id_initials") + self.assertEqual(initials.get_attribute("placeholder"), "Initials") # No longer required for new users # phone = self.browser.find_element(By.ID, 'id_phone') # self.assertEqual(phone.get_attribute('placeholder'), 'Phone') # Fill the form out incorrectly - username.send_keys('TestUsername') - email.send_keys('test@example.com') - password1.send_keys('correcthorsebatterystaple') + username.send_keys("TestUsername") + email.send_keys("test@example.com") + password1.send_keys("correcthorsebatterystaple") # deliberate mistake - password2.send_keys('correcthorsebatterystapleerror') - first_name.send_keys('John') - last_name.send_keys('Smith') - initials.send_keys('JS') + password2.send_keys("correcthorsebatterystapleerror") + first_name.send_keys("John") + last_name.send_keys("Smith") + initials.send_keys("JS") # phone.send_keys('0123456789') time.sleep(1) self.browser.switch_to.frame(self.browser.find_element(By.TAG_NAME, "iframe")) - self.browser.find_element(By.ID, 'anchor').click() + self.browser.find_element(By.ID, "anchor").click() self.browser.switch_to.default_content() time.sleep(3) # Submit incorrect form submit = self.browser.find_element(By.XPATH, "//input[@type='submit']") submit.click() + time.sleep(2) # Restablish error fields - password1 = self.browser.find_element(By.ID, 'id_password1') - password2 = self.browser.find_element(By.ID, 'id_password2') + password1 = self.browser.find_element(By.ID, "id_password1") + password2 = self.browser.find_element(By.ID, "id_password2") # Read what the error is - alert = self.browser.find_element(By.CSS_SELECTOR, '.alert-danger').text + alert = self.browser.find_element(By.CSS_SELECTOR, ".alert-danger").text # TODO Use regex matching to handle smart/unsmart quotes... self.assertIn("password fields didn", alert) # Passwords should be empty - self.assertEqual(password1.get_attribute('value'), '') - self.assertEqual(password2.get_attribute('value'), '') + self.assertEqual(password1.get_attribute("value"), "") + self.assertEqual(password2.get_attribute("value"), "") # Correct error - password1.send_keys('correcthorsebatterystaple') - password2.send_keys('correcthorsebatterystaple') + password1.send_keys("correcthorsebatterystaple") + password2.send_keys("correcthorsebatterystaple") # Submit again password2.send_keys(Keys.ENTER) # Check we have a success message - alert = self.browser.find_element(By.CSS_SELECTOR, '.alert-success').text - self.assertIn('register', alert) - self.assertIn('email', alert) + alert = self.browser.find_element(By.CSS_SELECTOR, ".alert-success").text + self.assertIn("register", alert) + self.assertIn("email", alert) # Check Email self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertIn('John Smith "JS" activation required', email.subject) - urls = re.findall( - r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body) + urls = re.findall(r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", email.body) self.assertEqual(len(urls), 1) mail.outbox = [] # empty this for later @@ -110,20 +108,20 @@ class UserRegistrationTest(LiveServerTestCase): self.browser.get(urls[0]) # go to the first link # Complete registration - title_text = self.browser.find_element(By.TAG_NAME, 'h2').text - self.assertIn('Complete', title_text) + title_text = self.browser.find_element(By.TAG_NAME, "h2").text + self.assertIn("Complete", title_text) # Test login - self.browser.get(self.live_server_url + '/user/login') - username = self.browser.find_element(By.ID, 'id_username') - self.assertEqual(username.get_attribute('placeholder'), 'Username') - password = self.browser.find_element(By.ID, 'id_password') - self.assertEqual(password.get_attribute('placeholder'), 'Password') - self.assertEqual(password.get_attribute('type'), 'password') + self.browser.get(self.live_server_url + "/user/login") + username = self.browser.find_element(By.ID, "id_username") + self.assertEqual(username.get_attribute("placeholder"), "Username") + password = self.browser.find_element(By.ID, "id_password") + self.assertEqual(password.get_attribute("placeholder"), "Password") + self.assertEqual(password.get_attribute("type"), "password") # Expected to fail as not approved - username.send_keys('TestUsername') - password.send_keys('correcthorsebatterystaple') + username.send_keys("TestUsername") + password.send_keys("correcthorsebatterystaple") password.send_keys(Keys.ENTER) # Test approval @@ -131,7 +129,7 @@ class UserRegistrationTest(LiveServerTestCase): self.assertFalse(profileObject.is_approved) # Read what the error is - alert = self.browser.find_element(By.CSS_SELECTOR, 'div.alert-danger').text + alert = self.browser.find_element(By.CSS_SELECTOR, "div.alert-danger").text self.assertIn("approved", alert) # Approve the user so we can proceed @@ -139,23 +137,24 @@ class UserRegistrationTest(LiveServerTestCase): profileObject.save() # Retry login - self.browser.get(self.live_server_url + '/user/login') - username = self.browser.find_element(By.ID, 'id_username') - username.send_keys('TestUsername') - password = self.browser.find_element(By.ID, 'id_password') - password.send_keys('correcthorsebatterystaple') + self.browser.get(self.live_server_url + "/user/login") + username = self.browser.find_element(By.ID, "id_username") + username.send_keys("TestUsername") + password = self.browser.find_element(By.ID, "id_password") + password.send_keys("correcthorsebatterystaple") password.send_keys(Keys.ENTER) + time.sleep(2) # Check we are logged in - udd = self.browser.find_element(By.CLASS_NAME, 'navbar').text - self.assertIn('Hi John', udd) + udd = self.browser.find_element(By.CLASS_NAME, "navbar").text + self.assertIn("Hi John", udd) # Check all the data actually got saved - self.assertEqual(profileObject.username, 'TestUsername') - self.assertEqual(profileObject.first_name, 'John') - self.assertEqual(profileObject.last_name, 'Smith') - self.assertEqual(profileObject.initials, 'JS') + self.assertEqual(profileObject.username, "TestUsername") + self.assertEqual(profileObject.first_name, "John") + self.assertEqual(profileObject.last_name, "Smith") + self.assertEqual(profileObject.initials, "JS") # self.assertEqual(profileObject.phone, '0123456789') - self.assertEqual(profileObject.email, 'test@example.com') + self.assertEqual(profileObject.email, "test@example.com") # All is well diff --git a/users/urls.py b/users/urls.py index c723297d..590b3594 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,7 +1,6 @@ -from django.conf.urls import include from django.contrib.auth.decorators import login_required from django.contrib.auth.views import LoginView -from django.urls import path +from django.urls import include, path from django.views.decorators.clickjacking import xframe_options_exempt from registration.backends.default.views import RegistrationView @@ -9,19 +8,22 @@ from PyRIGS.decorators import permission_required_with_403 from users import forms, views urlpatterns = [ - path('user/register/', RegistrationView.as_view(form_class=forms.ProfileRegistrationFormUniqueEmail), - name="registration_register"), - path('user/login/', LoginView.as_view(authentication_form=forms.CheckApprovedForm), name='login'), - path('user/login/embed/', xframe_options_exempt(views.LoginEmbed.as_view()), name='login_embed'), + path( + "user/register/", + RegistrationView.as_view(form_class=forms.ProfileRegistrationFormUniqueEmail), + name="registration_register", + ), + path("user/login/", LoginView.as_view(authentication_form=forms.CheckApprovedForm), name="login"), + path("user/login/embed/", xframe_options_exempt(views.LoginEmbed.as_view()), name="login_embed"), # User editing - path('user/edit/', login_required(views.ProfileUpdateSelf.as_view()), - name='profile_update_self'), - path('user/reset_api_key', login_required(views.ResetApiKey.as_view(permanent=False)), - name='reset_api_key'), - path('user/', login_required(views.ProfileDetail.as_view()), name='profile_detail'), - path('user//', - permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()), - name='profile_detail'), - path('user/', include('django.contrib.auth.urls')), - path('user/', include('registration.backends.default.urls')), + path("user/edit/", login_required(views.ProfileUpdateSelf.as_view()), name="profile_update_self"), + path("user/reset_api_key", login_required(views.ResetApiKey.as_view(permanent=False)), name="reset_api_key"), + path("user/", login_required(views.ProfileDetail.as_view()), name="profile_detail"), + path( + "user//", + permission_required_with_403("RIGS.view_profile")(views.ProfileDetail.as_view()), + name="profile_detail", + ), + path("user/", include("django.contrib.auth.urls")), + path("user/", include("registration.backends.default.urls")), ] diff --git a/users/views.py b/users/views.py index 24d25346..4628b07f 100644 --- a/users/views.py +++ b/users/views.py @@ -1,64 +1,63 @@ from django.contrib import messages from django.contrib.auth.views import LoginView +from django.contrib.auth import get_user_model from django.urls import reverse_lazy from django.views import generic from django.views.decorators.csrf import csrf_exempt -from RIGS import models - # This view should be exempt from requiring CSRF token. # Then we can check for it and show a nice error # Don't worry, django.contrib.auth.views.login will # check for it before logging the user in class LoginEmbed(LoginView): - template_name = 'registration/login_embed.html' + template_name = "registration/login_embed.html" @csrf_exempt def dispatch(self, request, *args, **kwargs): if request.method == "POST": - csrf_cookie = request.COOKIES.get('csrftoken', None) + csrf_cookie = request.COOKIES.get("csrftoken", None) if csrf_cookie is None: - messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.') - request.method = 'GET' # Render the page without trying to login + messages.warning(request, "Cookies do not seem to be enabled. Try logging in using a new tab.") + request.method = "GET" # Render the page without trying to login return super().dispatch(request, *args, **kwargs) class ProfileDetail(generic.DetailView): template_name = "profile_detail.html" - model = models.Profile + model = get_user_model() def get_queryset(self): try: - pk = self.kwargs['pk'] + pk = self.kwargs["pk"] except KeyError: pk = self.request.user.id - self.kwargs['pk'] = pk + self.kwargs["pk"] = pk return self.model.objects.filter(pk=pk) def get_context_data(self, **kwargs): context = super(ProfileDetail, self).get_context_data(**kwargs) - context['page_title'] = f"Profile: {self.object}" - context["completed_levels"] = self.object.level_qualifications.all().select_related('level') + context["page_title"] = f"Profile: {self.object}" + context["completed_levels"] = self.object.level_qualifications.all().select_related("level") return context class ProfileUpdateSelf(generic.UpdateView): template_name = "profile_form.html" - model = models.Profile - fields = ['first_name', 'last_name', 'email', 'initials', 'phone', 'dark_theme'] + model = get_user_model() + fields = ["first_name", "last_name", "email", "initials", "phone", "dark_theme"] def get_queryset(self): pk = self.request.user.id - self.kwargs['pk'] = pk + self.kwargs["pk"] = pk return self.model.objects.filter(pk=pk) def get_success_url(self): - url = reverse_lazy('profile_detail') + url = reverse_lazy("profile_detail") return url @@ -68,4 +67,4 @@ class ResetApiKey(generic.RedirectView): self.request.user.save() - return reverse_lazy('profile_detail') + return reverse_lazy("profile_detail") diff --git a/uv.lock b/uv.lock index 3d9a57a2..9bbe6ce9 100644 --- a/uv.lock +++ b/uv.lock @@ -1,23 +1,14 @@ version = 1 revision = 3 -requires-python = "==3.10.*" - -[[package]] -name = "ansicolors" -version = "1.1.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/31/7faed52088732704523c259e24c26ce6f2f33fbeff2ff59274560c27628e/ansicolors-1.1.8.zip", hash = "sha256:99f94f5e3348a0bcd43c82e5fc4414013ccc19d70bd939ad71e0133ce9c372e0", size = 23027, upload-time = "2017-06-02T21:22:10.729Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/18/a56e2fe47b259bb52201093a3a9d4a32014f9d85071ad07e9d60600890ca/ansicolors-1.1.8-py2.py3-none-any.whl", hash = "sha256:00d2dde5a675579325902536738dd27e4fac1fd68f773fe36c21044eb559e187", size = 13847, upload-time = "2017-06-02T21:22:12.67Z" }, -] +requires-python = "==3.14.*" [[package]] name = "asgiref" -version = "3.3.4" +version = "3.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/3f/ef696a6d8254f182b1a089aeffb638d2eb83055e603146d3a40605c5b7da/asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78", size = 30780, upload-time = "2021-04-06T18:40:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/bf/0f3ecda32f1cb3bf1dca480aca08a7a8a3bdc4bed2343a103f30731565c9/asgiref-3.9.2.tar.gz", hash = "sha256:a0249afacb66688ef258ffe503528360443e2b9a8d8c4581b6ebefa58c841ef1", size = 36894, upload-time = "2025-09-23T15:00:55.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/8b/05e225d11154b8f5358e6a6d277679c9741ec0339d1e451c9cef687a9170/asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee", size = 22678, upload-time = "2021-04-06T18:40:04.682Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" }, ] [[package]] @@ -29,6 +20,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "autopep8" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycodestyle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, +] + [[package]] name = "backports-tempfile" version = "1.0" @@ -52,50 +55,78 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.9.3" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/c3/d31704ae558dcca862e4ee8e8388f357af6c9d9acb0cad4ba0fbbd350d9a/beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25", size = 376031, upload-time = "2020-10-03T15:37:22.698Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/bb/3387a5bf72121e1e2cd9223d20598c2fb599ef63517ff3e6ec526d8c4928/beautifulsoup4-4.14.0.tar.gz", hash = "sha256:e6150e53c8a52fd4f3b9b28839f8f0fb7c7f029d3c953a50b1762b0947c3cf85", size = 625697, upload-time = "2025-09-27T17:22:17.532Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/41/e6495bd7d3781cee623ce23ea6ac73282a373088fcd0ddc809a047b18eae/beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666", size = 115807, upload-time = "2020-10-03T15:37:21.018Z" }, + { url = "https://files.pythonhosted.org/packages/28/4f/3e23dfc8b4951103028d30f29e17aa703a87564abd71bc405964c36326dc/beautifulsoup4-4.14.0-py3-none-any.whl", hash = "sha256:aee96fbccdf2d2a8d1288b2afa51fc76bb60823b7881a50fb1ed5f711d1a7d73", size = 106466, upload-time = "2025-09-27T17:22:16.13Z" }, +] + +[[package]] +name = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, ] [[package]] name = "brotli" -version = "1.0.9" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/18/70c32fe9357f3eea18598b23aa9ed29b1711c3001835f7cf99a9818985d0/Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438", size = 510202, upload-time = "2020-08-27T14:27:08.104Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/6d/371943e00e09969c8d50c3e076c1335fe1b07a2a8920e639726c9d857427/Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031", size = 786703, upload-time = "2021-08-11T17:39:02.255Z" }, - { url = "https://files.pythonhosted.org/packages/26/84/fefe2407f323a6a37b84212fc3caba85c537e83f7ecfa85d1c44e02808ff/Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43", size = 421825, upload-time = "2021-08-11T17:39:03.6Z" }, - { url = "https://files.pythonhosted.org/packages/20/70/76b8bda2fb5c498b2730f060ed28bbee27be0419a1824d09262f64e9114e/Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c", size = 2638220, upload-time = "2021-08-11T17:40:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1b/da5ed1a3b8b86b7adc83839ff5d832af9e01e661c976af3453be82a30f94/Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c", size = 2595107, upload-time = "2021-08-11T17:39:06.096Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d4/568fb9bb6819188317edd3f7bba157cafa3f1c1a87a2cbf73ff464a0c808/Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0", size = 2697824, upload-time = "2021-08-11T17:39:08.905Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d0/99ce3e68b6132900cafd8372a1717bdbe03ed5f858c1f0795a54e6c535bc/Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91", size = 3174662, upload-time = "2022-01-24T11:52:45.351Z" }, - { url = "https://files.pythonhosted.org/packages/62/39/c1ce8e26bd9c5f514c8c422797de83f0a1b474d766d3f846875394d093b7/Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa", size = 3152454, upload-time = "2022-01-24T11:52:53.912Z" }, - { url = "https://files.pythonhosted.org/packages/55/20/79ac7c3cc68e9a56b73c3b24474517e75fe39e0693c426e17b2631589e53/Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb", size = 3207211, upload-time = "2022-01-24T11:53:03.417Z" }, - { url = "https://files.pythonhosted.org/packages/cb/20/ccab7c3f1e4e90609f39f59e4eaa8b829d01a68ac4daa8e15f5c46354e73/Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181", size = 350702, upload-time = "2021-08-11T17:39:13.008Z" }, - { url = "https://files.pythonhosted.org/packages/6b/11/7d28cd3a5492fed09159e9b37613fdcec7655d93dec664b79d4bb4bb67b8/Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2", size = 383321, upload-time = "2021-08-11T17:39:10.739Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, ] [[package]] name = "cachetools" -version = "4.2.4" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/69/c457a860456cbf80ecc2e44ed4c201b49ec7ad124d769b71f6d0a7935dca/cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693", size = 25487, upload-time = "2021-09-30T10:26:22.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32", size = 30988, upload-time = "2025-08-25T18:57:30.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/c1/4740af52db75e6dbdd57fc7e9478439815bbac549c1c05881be27d19a17d/cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1", size = 10358, upload-time = "2021-09-30T10:26:20.649Z" }, + { url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6", size = 11276, upload-time = "2025-08-25T18:57:29.684Z" }, ] [[package]] name = "certifi" -version = "2025.8.3" +version = "2026.7.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] @@ -107,37 +138,74 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, -] - -[[package]] -name = "chardet" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/2d/9cdc2b527e127b4c9db64b86647d567985940ac3698eeabc7ffaccb4ea61/chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", size = 1907771, upload-time = "2020-12-10T19:35:33.971Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/c7/fa589626997dd07bd87d9269342ccb74b1720384a4d739a1872bd84fbe68/chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5", size = 178743, upload-time = "2020-12-10T19:35:32.469Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.3" +version = "3.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] @@ -149,56 +217,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "configparser" -version = "5.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/9c/c1ac39b3c72a70e93479cb4b7f1123f693293c5e4c40fdb3e1242f740665/configparser-5.0.2.tar.gz", hash = "sha256:85d5de102cfe6d14a5172676f09d19c465ce63d6019cf0a4ef13385fc535e828", size = 71248, upload-time = "2021-02-28T20:21:44.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/01/ff260a18caaf4457eb028c96eeb405c4a230ca06c8ec9c1379f813caa52e/configparser-5.0.2-py3-none-any.whl", hash = "sha256:af59f2cdd7efbdd5d111c1976ecd0b82db9066653362f0962d7bf1d3ab89a1fa", size = 19581, upload-time = "2021-02-28T20:21:43.861Z" }, -] - -[[package]] -name = "contextlib2" -version = "0.6.0.post1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/54/669207eb72e3d8ae8b38aa1f0703ee87a0e9f88f30d3c0a47bebdb6de242/contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e", size = 29670, upload-time = "2019-10-10T12:48:44.918Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/60/370352f7ef6aa96c52fb001831622f50f923c1d575427d021b8ab3311236/contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b", size = 9770, upload-time = "2019-10-10T12:47:48.72Z" }, -] - [[package]] name = "coverage" version = "7.10.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, - { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, ] -[package.optional-dependencies] -toml = [ - { name = "tomli" }, -] - [[package]] name = "coveralls" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "docopt" }, { name = "requests" }, ] @@ -209,11 +268,11 @@ wheels = [ [[package]] name = "cssselect" -version = "1.1.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/54/37630f6eb2c214cdee2ae56b7287394c8aa2f3bafb8b4eb8c3791aae7a14/cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc", size = 22718, upload-time = "2019-08-09T09:20:32.494Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/0a/c3ea9573b1dc2e151abfe88c7fe0c26d1892fe6ed02d0cdb30f0d57029d5/cssselect-1.3.0.tar.gz", hash = "sha256:57f8a99424cfab289a1b6a816a43075a4b00948c86b4dcf3ef4ee7e15f7ab0c7", size = 42870, upload-time = "2025-03-10T09:30:29.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/d4/3b5c17f00cce85b9a1e6f91096e1cc8e8ede2e1be8e96b87ce1ed09e92c5/cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf", size = 16647, upload-time = "2019-08-09T09:20:30.648Z" }, + { url = "https://files.pythonhosted.org/packages/ee/58/257350f7db99b4ae12b614a36256d9cc870d71d9e451e79c2dc3b23d7c3c/cssselect-1.3.0-py3-none-any.whl", hash = "sha256:56d1bf3e198080cc1667e137bc51de9cadfca259f03c2d4e09037b3e01e30f0d", size = 18786, upload-time = "2025-03-10T09:30:28.048Z" }, ] [[package]] @@ -231,11 +290,38 @@ wheels = [ [[package]] name = "cssutils" -version = "1.0.2" +version = "2.11.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/0b/c5f29d29c037e97043770b5e7c740b6252993e4b57f029b3cd03c78ddfec/cssutils-1.0.2.tar.gz", hash = "sha256:a2fcf06467553038e98fea9cfe36af2bf14063eb147a70958cfcaa8f5786acaf", size = 367038, upload-time = "2017-03-04T16:42:22.8Z" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/9f/329d26121fe165be44b1dfff21aa0dc348f04633931f1d20ed6cf448a236/cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2", size = 711657, upload-time = "2024-06-04T15:51:39.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/15/a9fb9010f58d1c55dd0b7779db2334feb9a572d407024f39a60f44293861/cssutils-1.0.2-py3-none-any.whl", hash = "sha256:c74dbe19c92f5052774eadb15136263548dd013250f1ed1027988e7fef125c8d", size = 406304, upload-time = "2017-03-04T16:41:55.332Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ec/bb273b7208c606890dc36540fe667d06ce840a6f62f9fae7e658fcdc90fb/cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1", size = 385747, upload-time = "2024-06-04T15:51:37.499Z" }, +] + +[[package]] +name = "cyclonedx-python-lib" +version = "11.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/40/6509e6cfd7f2f3255501690f46375fdd224949e21fd1e96f4f4c8a9041b1/cyclonedx_python_lib-11.12.0.tar.gz", hash = "sha256:16767c4039de90c04e9f03348f8f0ed4b8ff842eaa7eefcad3a95685f970dacf", size = 1445378, upload-time = "2026-08-13T07:52:18.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/f0/b2cb999244f2f4194df63ecff6939228178fa63a33be809c412684ca8db7/cyclonedx_python_lib-11.12.0-py3-none-any.whl", hash = "sha256:0e807521a921a5c3cb8ce1153f8a61d29eedfe76a46aac2796b7c6b573391a54", size = 529453, upload-time = "2026-08-13T07:52:16.836Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] @@ -259,36 +345,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/bb/2aa9b46a01197398b901e458974c20ed107935c26e44e37ad5b0e5511e44/diff_match_patch-20241021-py3-none-any.whl", hash = "sha256:93cea333fb8b2bc0d181b0de5e16df50dd344ce64828226bda07728818936782", size = 43252, upload-time = "2024-10-21T19:41:19.914Z" }, ] -[[package]] -name = "dj-database-url" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/c4/98fbf678e810029be8078419f7bba626aafa2e81bc38748757db954c477c/dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163", size = 5482, upload-time = "2018-03-01T18:39:51.842Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/a6/4b8578c1848690d0c307c7c0596af2077536c9ef2a04d42b00fabaa7e49d/dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9", size = 5502, upload-time = "2018-03-01T18:39:49.412Z" }, -] - -[[package]] -name = "dj-static" -version = "0.0.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "static3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/8f/77a4b8ec50c821193bf9682c7896f12fd0418eb3711a7d66796ede59c23b/dj-static-0.0.6.tar.gz", hash = "sha256:032ec1c532617922e6e3e956d504a6fb1acce4fc1c7c94612d0fda21828ce8ef", size = 3368, upload-time = "2014-07-11T17:14:41.094Z" } - [[package]] name = "django" -version = "3.2.25" +version = "5.2.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, - { name = "pytz" }, { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/68/0e744f07b57bfdf99abbb6b3eb14fcba188867021c05f4a104e04f6d56b8/Django-3.2.25.tar.gz", hash = "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", size = 9836336, upload-time = "2024-03-04T08:57:02.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/d8/43e9d000519adceb189620b6869ff88031e046df91c2e9da72f8f6918399/django-5.2.17.tar.gz", hash = "sha256:9d4d93be539a18ab80d058eb515900e10951e04c537c5a6b394fc49528d3251f", size = 10889740, upload-time = "2026-08-04T15:04:03.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/8e/cc23c762c5dcd1d367d73cf006a326e0df2bd0e785cba18b658b39904c1e/Django-3.2.25-py3-none-any.whl", hash = "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38", size = 7890550, upload-time = "2024-03-04T08:56:47.529Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/ce120525ca78f12b07daf65786679c5d0b54a75285a8958d3ae55e39da35/django-5.2.17-py3-none-any.whl", hash = "sha256:f04fb3b36ee119e1af4fa1d397d5fd6cf12700f49321e84d4f4c642c5b1973db", size = 8315563, upload-time = "2026-08-04T15:03:59.1Z" }, ] [[package]] @@ -303,31 +371,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/53/f650a07c51be8cffb63c472d6d2836a7b3aabc1fd7b94d4bafedfa68e353/django_coverage_plugin-3.1.1-py3-none-any.whl", hash = "sha256:5e6437f24aef66e36edb912f5992954cef9f8088b75227bcb73270128a7069fc", size = 14729, upload-time = "2025-06-15T14:49:58.686Z" }, ] -[[package]] -name = "django-debug-toolbar" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, - { name = "sqlparse" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/e0/85387258279b436e66d0afa5e676b5956ae9f68e62e22fadcc473315e322/django_debug_toolbar-4.0.0.tar.gz", hash = "sha256:89619f6e0ea1057dca47bfc429ed99b237ef70074dabc065a7faa5f00e1459cf", size = 115500, upload-time = "2023-04-03T14:48:15.352Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/85/4a70b9f31cb01ebec664e2abdebba94aa8c0ae8e029b2b8564d6316e3bf1/django_debug_toolbar-4.0.0-py3-none-any.whl", hash = "sha256:bad339d68520652ddc1580c76f136fcbc3e020fd5ed96510a89a02ec81bb3fb1", size = 221440, upload-time = "2023-04-03T14:48:51.712Z" }, -] - -[[package]] -name = "django-filter" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7b/cf/adae3e55995ea27e1dceb493e0226557d4207d8819ddb99591df5204a471/django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06", size = 146904, upload-time = "2020-09-27T09:08:58.079Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/2b/b2fe483c3095b6222725dd05f9ad9e6ed6cb7347c154fdbd80238d36f1a8/django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1", size = 73156, upload-time = "2020-09-27T09:08:52.69Z" }, -] - [[package]] name = "django-hcaptcha" version = "0.2.0" @@ -349,16 +392,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/52/37/df97e6f60662e2545 [[package]] name = "django-ical" -version = "1.7.3" +version = "1.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "django-recurrence" }, { name = "icalendar" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/9d/e1de978a985a49c26180f37d435c73d371543b3b1fe1bd0cd44ec99f6b57/django-ical-1.7.3.tar.gz", hash = "sha256:bd5c874d2eb81329f220174cc0dde7be385f4574ce6c8a2d1579d7fd564a94f3", size = 26894, upload-time = "2021-05-03T15:01:17.356Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/dc/70971de377f77c4f9f789a38b049b5fa239f1cfc6b671b0503971960f245/django-ical-1.9.2.tar.gz", hash = "sha256:44c9b6fa90d09f25e9ebaa91ed9eb007f079afbc23d6aac909cfc18188a8e90c", size = 30831, upload-time = "2023-06-12T15:22:54.015Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/6a/e48bbf5f0bce8277eb793a7d5a52e72831fbc775dea26ca95b399238ade1/django_ical-1.7.3-py2.py3-none-any.whl", hash = "sha256:6df4dc61eb4abc55816bd16a949e497bea99828c7de648438ace7f1f85eeb405", size = 14109, upload-time = "2021-05-03T15:01:12.106Z" }, + { url = "https://files.pythonhosted.org/packages/05/3f/647857462fa4169cb855aaf14969a6843f3910a3781d7c17f18c460b0e08/django_ical-1.9.2-py3-none-any.whl", hash = "sha256:74a16bca05735f91a00120cad7250f3c3aa292a9f698a6cfdc544a922c11de70", size = 15494, upload-time = "2023-06-12T15:22:37.618Z" }, ] [[package]] @@ -388,16 +431,15 @@ wheels = [ [[package]] name = "django-recurrence" -version = "1.10.3" +version = "1.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, { name = "python-dateutil" }, - { name = "pytz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/73/ae63c3aec07512147b39ff29a401922ad6e1474b18f5cc7b8e2ff7e36847/django-recurrence-1.10.3.tar.gz", hash = "sha256:715f681f6af029ff3a8d73c7b1460abd8cbc5d5a5001efcb127032e84d9cb963", size = 123281, upload-time = "2020-05-10T19:12:58.691Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/c7/e8d8539d8ccb3ee6498206b6ecef6cd551c3a281e28ae16812b9bc868da3/django-recurrence-1.11.1.tar.gz", hash = "sha256:9c89444e651a78c587f352c5f63eda48ab2f53996347b9fcdff2d248f4fcff70", size = 133440, upload-time = "2022-01-25T10:14:06.734Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/0a/ecb4a5ec001a552c7b0b6f51546300ed6146bd635d3f95880111fed73049/django_recurrence-1.10.3-py3-none-any.whl", hash = "sha256:9053b44b78b7fbfe3530673edfdd6d2f562105f8a192bc6a4b906a3df4f95f59", size = 124586, upload-time = "2020-05-10T19:12:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/3d/0a/5dc0f1f408a5b5f00c64ec57f3868248ed65ee660980118867b1279eca96/django_recurrence-1.11.1-py3-none-any.whl", hash = "sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5", size = 127256, upload-time = "2022-01-25T10:14:05.319Z" }, ] [[package]] @@ -411,23 +453,23 @@ wheels = [ [[package]] name = "django-reversion" -version = "3.0.9" +version = "6.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "django" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/d4/ee328caa4b4dfd1cec2185c63cc349f3d45de6293920889273ae0c6db3bd/django-reversion-3.0.9.tar.gz", hash = "sha256:a5af55f086a3f9c38be2f049c251e06005b9ed48ba7a109473736b1fc95a066f", size = 70781, upload-time = "2021-01-22T10:47:21.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/df/5870e38067fd80b3d749de2d11a4cbd32be811d1f43eb8ab948f9f3152c4/django_reversion-6.0.0.tar.gz", hash = "sha256:cab0fd90640b2ceb37d7a897823ca72a21b960ad1d6a3b9cb4e351fabbd27d9c", size = 74528, upload-time = "2025-09-20T08:56:20.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/f5/53f0070659ee93e4bc8b26209a3316855d42aa98a161669c6687bd8ed889/django_reversion-3.0.9-py3-none-any.whl", hash = "sha256:1b57127a136b969f4b843a915c72af271febe7f336469db6c27121f8adcad35c", size = 85178, upload-time = "2021-01-22T10:47:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0d/7e558eb30afcdb21eca15a6ef8aa048b6089efc4599e28611275a0643260/django_reversion-6.0.0-py3-none-any.whl", hash = "sha256:0755b4b3e615254b305d01012e426e09006455d375254e5adc29572fc9edf2ab", size = 85968, upload-time = "2025-09-20T08:56:18.761Z" }, ] [[package]] name = "django-widget-tweaks" -version = "1.4.12" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/68/64437dd92338064d3cd70030df25ee35c2cfee25522d1717de3ee8ed104a/django-widget-tweaks-1.4.12.tar.gz", hash = "sha256:9bfc5c705684754a83cc81da328b39ad1b80f32bd0f4340e2a810cbab4b0c00e", size = 14675, upload-time = "2022-01-13T10:53:23.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/fe/26eb92fba83844e71bbec0ced7fc2e843e5990020e3cc676925204031654/django-widget-tweaks-1.5.0.tar.gz", hash = "sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7", size = 14767, upload-time = "2023-08-25T15:29:12.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/bb/855748bfc347e6df94cae36a6da0043620e148b3965c33a79e5ad4af7036/django_widget_tweaks-1.4.12-py3-none-any.whl", hash = "sha256:fe6b17d5d595c63331f300917980db2afcf71f240ab9341b954aea8f45d25b9a", size = 8924, upload-time = "2022-01-13T10:53:21.734Z" }, + { url = "https://files.pythonhosted.org/packages/46/6a/6cb6deb5c38b785c77c3ba66f53051eada49205979c407323eb666930915/django_widget_tweaks-1.5.0-py3-none-any.whl", hash = "sha256:a41b7b2f05bd44d673d11ebd6c09a96f1d013ee98121cb98c384fe84e33b881e", size = 8960, upload-time = "2023-08-25T15:29:05.644Z" }, ] [[package]] @@ -442,18 +484,6 @@ version = "0.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/8d/bee8a59732c169a455627ff1557d0db180f7c352b0274480267ad3e46875/envparse-0.2.0.tar.gz", hash = "sha256:4f3b9a27bb55d27f124eb4adf006fec05e4588891c9a054a183a112645056eb7", size = 7576, upload-time = "2015-12-19T17:24:03.234Z" } -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - [[package]] name = "execnet" version = "2.1.1" @@ -463,6 +493,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" }, ] +[[package]] +name = "filelock" +version = "3.32.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" }, +] + [[package]] name = "freetype-py" version = "2.5.1" @@ -479,14 +518,11 @@ wheels = [ [[package]] name = "gunicorn" -version = "22.0.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1e/88/e2f93c5738a4c1f56a458fc7a5b1676fc31dcdbb182bef6b40a141c17d66/gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63", size = 3639760, upload-time = "2024-04-16T22:58:19.218Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b8/ec4ba3f6cace4091c34e27478b576bb80f2f06fab80fd42c0ecc785b308f/gunicorn-26.1.0.tar.gz", hash = "sha256:1413d777bf99d31ebeb08acd354b01f1ecc44db0aa7b811ae7b86c669232e4f7", size = 755923, upload-time = "2026-08-18T11:49:39.438Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/97/6d610ae77b5633d24b69c2ff1ac3044e0e565ecbd1ec188f02c45073054c/gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", size = 84443, upload-time = "2024-04-16T22:58:15.233Z" }, + { url = "https://files.pythonhosted.org/packages/19/dc/7a55fc605543fd5cb11c003fbbb21a1911d5e88a582cce6c5e063bf5c176/gunicorn-26.1.0-py3-none-any.whl", hash = "sha256:9f45bcddec5e9dc7a25a3bdccb0c6832f11fd5d4739b1ee36c8d2fec25f1dc86", size = 216237, upload-time = "2026-08-18T11:49:38.001Z" }, ] [[package]] @@ -513,36 +549,24 @@ wheels = [ [[package]] name = "icalendar" -version = "4.0.9" +version = "6.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, - { name = "pytz" }, + { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/4b/b9e53b2fc4f7942c5698842005ad9e55dd4d932434f5bd1cf8acf3d06b01/icalendar-4.0.9.tar.gz", hash = "sha256:cc73fa9c848744843046228cb66ea86cd8c18d73a51b140f7c003f760b84a997", size = 84254, upload-time = "2021-10-16T13:24:15.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/13/e5899c916dcf1343ea65823eb7278d3e1a1d679f383f6409380594b5f322/icalendar-6.3.1.tar.gz", hash = "sha256:a697ce7b678072941e519f2745704fc29d78ef92a2dc53d9108ba6a04aeba466", size = 177169, upload-time = "2025-05-20T07:42:50.683Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/9e/761f42be52bf4d229c0f04c788d8f9c585c47d198a2c5b0b108d4447ed11/icalendar-4.0.9-py2.py3-none-any.whl", hash = "sha256:cf1446ffdf1b6ad469451a8966cfa7694f5fac796ac6fc7cd93e28c51a637d2c", size = 74633, upload-time = "2021-10-16T13:24:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/b5fc00e85d2dfaf5c806ac8b5f1de072fa11630c5b15b4ae5bbc228abd51/icalendar-6.3.1-py3-none-any.whl", hash = "sha256:7ea1d1b212df685353f74cdc6ec9646bf42fa557d1746ea645ce8779fdfbecdd", size = 242349, upload-time = "2025-05-20T07:42:48.589Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/bd/fa8ce65b0a7d4b6d143ec23b0f5fd3f7ab80121078c465bc02baeaab22dc/importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5", size = 54320, upload-time = "2024-08-20T17:11:42.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/14/362d31bf1076b21e1bcdcb0dc61944822ff263937b804a79231df2774d28/importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", size = 26269, upload-time = "2024-08-20T17:11:41.102Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -555,61 +579,128 @@ wheels = [ ] [[package]] -name = "lxml" -version = "6.0.2" +name = "license-expression" +version = "30.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, - { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, - { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, - { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, - { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, - { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, - { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, - { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, - { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + +[[package]] +name = "lxml" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/a9/970b8fa0ecc4fbf1dfaed0d89bbc1fc1421b25ec26a2038c91e872dc6c8e/lxml-6.1.2.tar.gz", hash = "sha256:1055241852f2b02068af4a625a5d32c087db193c12251928af2562ecd2239f18", size = 4210626, upload-time = "2026-08-19T04:58:15.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/b5/728b0578791b397ace8d1b101c8b3fe10f36043542f7bb85f82d8bdc3f50/lxml-6.1.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d50a44113fe6800dcc8a859332b823a4735b1e6ae1b0063882e4cca569ec3e29", size = 8609651, upload-time = "2026-08-19T04:58:42.42Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/49209fa6225c15c48a30061f03d3aba75e3c19634813b88bf83b88c525ed/lxml-6.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fa813b0247d0543a563b993ac3dba6168eef59e3a61448432cf5453300c2412b", size = 4639588, upload-time = "2026-08-19T04:59:01.501Z" }, + { url = "https://files.pythonhosted.org/packages/20/86/80bae4e8bc2eed9d6f017701a3d86fdea56936218efa738911d0b76aa7f4/lxml-6.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d858e718b94033ab4b67e4a58fe3114c65bae01ae2314a62fb39ae8897ed4324", size = 4964846, upload-time = "2026-08-19T04:59:08.59Z" }, + { url = "https://files.pythonhosted.org/packages/70/ce/4782caee7a22959c1ac67cb46495e03912c22a4ba7d20c163496a519e815/lxml-6.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e3b666f57a5d81562f38c766c762416b0f6eb58a00590546911514b48412abd", size = 5099288, upload-time = "2026-08-19T04:59:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/21/f120967cc43b54e05512dff0c39726b832c836195d30f41f88733ef36ac8/lxml-6.1.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26ff164c6629e5c4d11c9e55d5ea3d6eed0be2a420eee1f55cbce6e2c23e231a", size = 5036837, upload-time = "2026-08-19T04:59:47.217Z" }, + { url = "https://files.pythonhosted.org/packages/61/ba/8005e9f47598e3ec5c18312c77f94e889580027616678848405c6aeba5de/lxml-6.1.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:962c12b51d0b164f12569af225dea57568477e24a845b96eaccbef6c07e4cc03", size = 5658569, upload-time = "2026-08-19T04:59:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ba/add33b3c7ce51462cf7a4637bcfec2eaa258364d6015b989dd7d1216e6a6/lxml-6.1.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47e367dfe341521426692819803e260d0673899c0ff611f14af978d725e2c999", size = 5246003, upload-time = "2026-08-19T04:59:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/05/b3/a43012748fb861c914c5eac1c1a3bad44282e767499cd02280d4d1edf092/lxml-6.1.2-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:92c2b366028ac01e90399e6d17734ce6e4f4aeddd8ba75fbaf80ea11d6c6d645", size = 5354047, upload-time = "2026-08-19T05:00:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cb/813021d9a445713b8d758b9e5eae2ed392cd598d9f119d9b053b37c2ab93/lxml-6.1.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:7e81fc065ede5d58dd0bf0912025aee1bd04c52c2affd61fdb93226a97ce2fc6", size = 4704382, upload-time = "2026-08-19T05:00:47.067Z" }, + { url = "https://files.pythonhosted.org/packages/17/c9/1155299f4577bebf3c280497534a73e4b8ad8cab3b96074731ad10949d4e/lxml-6.1.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:633ac039cb32366dd5935868e041e385875c017b8cd54ea56aeee3fe29ca5935", size = 5258530, upload-time = "2026-08-19T05:01:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/25/6e/d76e58384b378b877e140e25b9a9835da00035f81ff70cbe943a3749bf27/lxml-6.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f3194777c0d05945ac91d8594be25d2679d1d826e01e1fc90bae568ff3a547b", size = 5089919, upload-time = "2026-08-19T05:01:33.602Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b7/898013c0f8891481d0624ab3bd5dd8c8ff827232dfee2a5d1f8bf970a7cc/lxml-6.1.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1133bd969f2bfcc6b0c0cf7cdf5f2631e62b23fa2471ee8bd44f6ab73554ee9a", size = 4741972, upload-time = "2026-08-19T05:01:38.18Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/efb53c4d7b655831c03317a450d9da439b0829c61f34d9d4fe7c863445d6/lxml-6.1.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1edca8f4a92b94e873093df959f141d388f2141fcad0c47598442fb4730ef57a", size = 5683241, upload-time = "2026-08-19T05:02:00.731Z" }, + { url = "https://files.pythonhosted.org/packages/da/0a/0ff36a584cbba14a71326ee8a5300694400f0b97927d1f90a87d95b17d4a/lxml-6.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8512b3775d68994dd1d6d533161e0a214f2ad9c634659d34a99c98e86c6c3d68", size = 5245892, upload-time = "2026-08-19T05:02:06.108Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9e/303717a1aa56d4bd775c91936717d3c9e8d999a8e8b68b00979c4c1f93d0/lxml-6.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5005c0c9e4d749a76a2ff8bd5918a8bb248df8e08e73a55654b9f79c9cd1e2b", size = 5269528, upload-time = "2026-08-19T05:02:09.883Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/2ae7cb97089eb86bf0689516db3cf280a007b6145853d2a0235a1f01683d/lxml-6.1.2-cp314-cp314-win32.whl", hash = "sha256:e17e2c30e27f56da5551e7a425888b45f013e940b99ab07d125a1c33f77a4605", size = 3662743, upload-time = "2026-08-19T05:03:02.513Z" }, + { url = "https://files.pythonhosted.org/packages/77/13/a3d483230a09201e211ceb1aa208b1374d27d23b8b180d74dba14b30f6b3/lxml-6.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:87e9673cd8a3445024fe38e7f91b55fa3428437eec9b7a7ff7d81979520c0d2d", size = 4073942, upload-time = "2026-08-19T05:03:04.864Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f1/c1445d4b6ad7c51e39d4e2ebbf015a4880f5b297a4ab0e77e4d0e5b70110/lxml-6.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:878e7c8ada8f92c52f13f35a2ab98ef0adf7fd0211d164fc2af589e4c3cfed63", size = 3749235, upload-time = "2026-08-19T05:03:07.239Z" }, + { url = "https://files.pythonhosted.org/packages/9d/eb/598c76f4ce19a67c635e86a46d880cc854f308f39a6f1fdf13bbb01813ec/lxml-6.1.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:94162456ed0a64fb1c06915df5bd06af4675ae3966d6048fcb73b0906e0e0222", size = 8860315, upload-time = "2026-08-19T05:02:14.39Z" }, + { url = "https://files.pythonhosted.org/packages/da/c7/1f9fac7b566a86ad0da13dcc0259164266469c0ad86744c740ccd5c2a081/lxml-6.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4b0fa7109b1d0bc1747d8241a0853e135eefb1c978685241b544c46937383efd", size = 4755176, upload-time = "2026-08-19T05:02:18.705Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1b/cfda9307388d496e7eeb7493d9455896b8137ed95f51f3d6ae6ddcc14a47/lxml-6.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:604f4778632588d7c000e7e19430639dc12fca58b5b6e99edffba7631725ef0e", size = 4979444, upload-time = "2026-08-19T05:02:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/e5/71/f732c8919c45b7f29acf443288c6e90036877a67bfeeb1acceb0fffa011b/lxml-6.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a096d6a5f96b776a5b020cb45c17c545effd2a3b6639e6fa97bc95537600923", size = 5115887, upload-time = "2026-08-19T05:02:23.62Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/121d52b944f41e33ea86c62875f902d24982842dc7231ab154ac5a6c6593/lxml-6.1.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6454d184d556eaf4cb3d6f69e405d21602d6fdcf08b8d57796824275986c6595", size = 5032418, upload-time = "2026-08-19T05:02:26.114Z" }, + { url = "https://files.pythonhosted.org/packages/70/19/cadb73c7fe48c7563dc8ab62ea53d5b920c8911bfb808507a6daa82e78d2/lxml-6.1.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b68f2548259bb04e0b3d5df0c397abe8b0080f5e1ffe4019fb7a8bf01a9339e", size = 5603304, upload-time = "2026-08-19T05:02:28.694Z" }, + { url = "https://files.pythonhosted.org/packages/13/32/9de126a14d5a5db8c371c5ec869178417db226707b62a47273a95ae6df7f/lxml-6.1.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c9cc4b6532abe154dbdebb42aaba8d52c852919591e45067f5b7d46a0405e88", size = 5228938, upload-time = "2026-08-19T05:02:30.99Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/22dd9e843629ed04652591fb220eb2bf2394d97be3be377d60d8083405d7/lxml-6.1.2-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:57188e441ab24f906bd5a5c14eb55363ab51aa6c0de549f3dd320043721cc118", size = 5317790, upload-time = "2026-08-19T05:02:33.301Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/b12a1dc121f81c280635c721c7bcaa341441fcbe37397f60b8915048aece/lxml-6.1.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d0bfd719c254bbe60ea022cff0e6ffb799a6fa7d4d72852cebe0257957b32d68", size = 4646468, upload-time = "2026-08-19T05:02:35.504Z" }, + { url = "https://files.pythonhosted.org/packages/57/41/fd87a41edc531e7969c25ab1d6b52b5b041eb108b88f6394d6afb4374396/lxml-6.1.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:be6f87cd224254a8f81324e34cc655508b83f1d70458a1a39857ad2aa9925852", size = 5240607, upload-time = "2026-08-19T05:02:37.805Z" }, + { url = "https://files.pythonhosted.org/packages/6e/30/713ba813b6e6673c6dc34733746516017efcd17949b767b154cc50bccf20/lxml-6.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:074a88f70a7360a4a0c5be5d898062cd26f898c25b459efb1bdd43ae700c5a1a", size = 5086495, upload-time = "2026-08-19T05:02:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/33/f8/6532ce0fecd9c326d06b08274ee075cc28dbc9f5e9285355db8504689114/lxml-6.1.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:9031f5f01452681abf39fdd65f84a70cb01a7572a1bbf570042e826b1232d07b", size = 4758801, upload-time = "2026-08-19T05:02:45.434Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/5a1f7833ebaa0dd33c28f6f9755ec6ff3891bf63f097634b44e6da1bb65e/lxml-6.1.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:cfeac14425fc7a6fca7864b774d4ee63547926158f4a18c67d77b2c9a948acf1", size = 5626977, upload-time = "2026-08-19T05:02:48.092Z" }, + { url = "https://files.pythonhosted.org/packages/e6/20/6ae0fc1b45e20877cdcfb1168ceeaf9abb0fba5ed36bd639a260e7b2101e/lxml-6.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8ec111ff8067325f85c08aa9c2b26179ec0537bb89c003fde31127139f85f82d", size = 5235036, upload-time = "2026-08-19T05:02:50.726Z" }, + { url = "https://files.pythonhosted.org/packages/47/b4/2bc7b37fbb990ccfb7d30393660741592177224a94e07d842c8da70638e8/lxml-6.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48e912f37c99a297175ba955f55a47c0e1c834b506ef162e52a6e4fe276e6e45", size = 5252270, upload-time = "2026-08-19T05:02:53.454Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0b/07fb8e1dee29a78e2c5fa5c6c914218be76a6406baff27907429566e90ec/lxml-6.1.2-cp314-cp314t-win32.whl", hash = "sha256:7c444c3a6e8e75334879980eed96568f0e12064c8b1913424eac1805e976736b", size = 3902666, upload-time = "2026-08-19T05:02:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/3371527bd9820aae6f511697c93032ed197b0d8dab0f17818f18d3099637/lxml-6.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7f35ba7667004ecdafebbe08da7c9fa06ee6195275bb7ef7a29ee1901e69519c", size = 4401011, upload-time = "2026-08-19T05:02:57.899Z" }, + { url = "https://files.pythonhosted.org/packages/e6/bb/e6de9b2546a4e6df4fb52fb18921906a8b7a041aba06570995759a4d6d8b/lxml-6.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d117f39b28ab8a330a74abdbe61c2255b51973b238db25fd6c2448de1eb2a02d", size = 3823384, upload-time = "2026-08-19T05:03:00.371Z" }, ] [[package]] name = "markdown" -version = "3.3.7" +version = "3.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/58/79df20de6e67a83f0d0bbfe6c19bb82adf68cdf362885257eb01099f930a/Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874", size = 324130, upload-time = "2022-05-05T19:08:39.707Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/df/ca72f352e15b6f8ce32b74af029f1189abffb906f7c137501ffe69c98a65/Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621", size = 97778, upload-time = "2022-05-05T19:08:38.155Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] [[package]] name = "msgpack" -version = "1.0.8" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/4c/17adf86a8fbb02c144c7569dc4919483c01a2ac270307e2d59e1ce394087/msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", size = 167014, upload-time = "2024-03-02T01:19:21.299Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/c2/8ecbafd6d3178ad408989c82d6d518fec76e053bae20c0fd9f47bffe7dda/msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", size = 157691, upload-time = "2024-03-01T12:34:50.572Z" }, - { url = "https://files.pythonhosted.org/packages/0d/7e/93373ffbe6561e719996a90b6d112604f52da3ab46e7c395db7607458553/msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", size = 87954, upload-time = "2024-03-01T12:34:52.52Z" }, - { url = "https://files.pythonhosted.org/packages/ba/13/d000e53b067aee19d57a4f26d5bffed7890e6896538ac5f97605b0f64985/msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", size = 84945, upload-time = "2024-03-01T12:34:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6e/3dcd4f7d8b978277393fd5b7c0abd9d2b6ef7ba8eb12834bed59158ecf5f/msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", size = 376004, upload-time = "2024-03-01T12:34:57.709Z" }, - { url = "https://files.pythonhosted.org/packages/d9/96/a1868dd8997d65732476dfc70fef44d046c1b4dbe36ec1481ab744d87775/msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", size = 385107, upload-time = "2024-03-01T12:35:00.529Z" }, - { url = "https://files.pythonhosted.org/packages/9b/db/8d629233bba3cbe6d7a6e0fd018ed684c5f0befea4428d4217ce066d2f20/msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", size = 374290, upload-time = "2024-03-01T12:35:02.857Z" }, - { url = "https://files.pythonhosted.org/packages/f0/75/553cc9ddfe59c62654dd398c16cd8ab1b3eeb145e56805f52115cbe9f5a0/msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", size = 380759, upload-time = "2024-03-01T12:35:07.369Z" }, - { url = "https://files.pythonhosted.org/packages/7c/40/c6f31cef899b54e3f6a759204d0b152c9205aef7219c9d2279f608c421eb/msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", size = 413943, upload-time = "2024-03-01T12:35:10.271Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a8/29426f7af85406116e1cdbd21d8f02e30ef8f4afe3cfcbb43c498cbadadf/msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", size = 385405, upload-time = "2024-03-01T12:35:13.438Z" }, - { url = "https://files.pythonhosted.org/packages/98/b4/a32559cd8604402f55560ab7e5ebf20a92b533f376d693bb67a9c0aff41e/msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", size = 69043, upload-time = "2024-03-01T12:35:17.896Z" }, - { url = "https://files.pythonhosted.org/packages/21/47/b7217d54e15dbae5492b845364427fa3cb1b0ccb58160b04ba47b551d7d9/msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", size = 75106, upload-time = "2024-03-01T12:35:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] [[package]] @@ -624,6 +715,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, ] +[[package]] +name = "packageurl-python" +version = "0.17.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -633,18 +733,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "pep517" -version = "0.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "toml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/65/399b2e85e1ee5e34d797643b1d1ce3ec3f2b612e0680d6b7b455c24cb7a7/pep517-0.9.1.tar.gz", hash = "sha256:aeb78601f2d1aa461960b43add204cc7955667687fbcf9cdb5170f00556f117f", size = 23213, upload-time = "2020-10-18T20:41:40.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/f5/2411973d0032f62629908612fb961eb2987212e071dba6a0d9f86d20820e/pep517-0.9.1-py2.py3-none-any.whl", hash = "sha256:3985b91ebf576883efe5fa501f42a16de2607684f3797ddba7202b71b7d0da51", size = 18548, upload-time = "2020-10-18T20:41:39.027Z" }, -] - [[package]] name = "pikepdf" version = "9.11.0" @@ -657,34 +745,106 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/f5/4c/62b37a3ee301c245be6ad269ca771c2c5298bf049366e1094cfdf80d850c/pikepdf-9.11.0.tar.gz", hash = "sha256:5ad6bffba08849c21eee273ba0b6fcd4b6a9cff81bcbca6988f87a765ba62163", size = 4546289, upload-time = "2025-09-12T07:15:11.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/0f/443a152687cb110e4adb7d998b413d124830cc8967a74e5f236c244c352b/pikepdf-9.11.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:8ac1adbb2e32a1cefb9fc51f1e892de1ce0af506f040593384b3af973a46089b", size = 4989446, upload-time = "2025-09-12T07:13:44.401Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b4/a0f3208d2a95f75f1204bbb5a36f83441826fa8463edf92ff08810d4ed0b/pikepdf-9.11.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:f53ccda7be5aa7457a1b32b635a1e289dcdccb607b4fa7198a2c70e163fc0b8b", size = 4682716, upload-time = "2025-09-12T07:13:47.902Z" }, - { url = "https://files.pythonhosted.org/packages/a6/10/12a1f044b3e923a0998b0fb5f81265c4cbf0aa5f6e0d992782497241667e/pikepdf-9.11.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:491345765d819a9d9d4676bd55ccff15a043db794104325a181e1870ec511855", size = 2380569, upload-time = "2025-09-12T07:13:49.817Z" }, - { url = "https://files.pythonhosted.org/packages/91/3f/eec913d34c01076b02ccb5b897eae4381f95343a69e4a5e19d9783d667a3/pikepdf-9.11.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:501dd145a3e89ee25c612ae88530813f2612fe24abb178f2907d3cf7997a0719", size = 2597555, upload-time = "2025-09-12T07:13:51.459Z" }, - { url = "https://files.pythonhosted.org/packages/68/82/1d1d6e93d9a456d5309e79d17b32edf8f1faf635cb2106e36e4eccf67ddb/pikepdf-9.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab2980881f8a8e500a1ce27e16a69907a87fe0875894ed5269586012794d6bd6", size = 3573555, upload-time = "2025-09-12T07:13:53.2Z" }, - { url = "https://files.pythonhosted.org/packages/ce/92/2c90ea29c11a4cc0e522b32259c1326e6ed58a58d5cf35c5b3436800cc40/pikepdf-9.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eb5c579c1da45aa771d379eacf213daceb789055e11f851f662d17eafd56868e", size = 3757083, upload-time = "2025-09-12T07:13:55.337Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9c/e6a02cc24174954f6c8196d6f7a96f8bc40a7f9c831d65062372ba8fda43/pikepdf-9.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:7c62035466b0c5eabb1812f3ce5925312e2bb9e343a7e900a00c409e1ba89318", size = 3722540, upload-time = "2025-09-12T07:13:57.536Z" }, + { url = "https://files.pythonhosted.org/packages/e6/43/adfd1daf833d646ada849c4f3f50ad6032e8e82112595cbeacebcc25d1d8/pikepdf-9.11.0-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:b366aefe9a30caababfbdc9f4647c8d0b7e92cfe34b6399399b78d4b96db9004", size = 5003377, upload-time = "2025-09-12T07:14:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/5932cb5d884d360b253f0cb615f0f78f30c0a1d762c5a10760abf96a0811/pikepdf-9.11.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3aed8fa4dabbf8ac1d9f5b8c15fa0881040f21ae58b4436c7f51f43c2375fc77", size = 4692845, upload-time = "2025-09-12T07:14:44.299Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/a674468806890d10f2f931641f06aa7dac8400594a96a7baf4d9f1d8810e/pikepdf-9.11.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a09b060e52449080a87720d6af00f551f879e55c6d8e8884526e5434843fc15e", size = 2401793, upload-time = "2025-09-12T07:14:46.202Z" }, + { url = "https://files.pythonhosted.org/packages/1c/99/7727cd512db321ee81d20f96a072933677ed58ece03b449fe003ff78ed92/pikepdf-9.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42e5a69c32718b25da863ff3d408aa8bde677e19dbf8b05e6a12244f99c65f3", size = 2635642, upload-time = "2025-09-12T07:14:48.172Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a0/0ebde63512eb79851eda33c2e798ffcc4956b34a0e425f8185c18f27eb0c/pikepdf-9.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:82f628fcfd98f27b0feac273aa2e088e47bc6e2b22d73c6251449b6bc901244a", size = 3593088, upload-time = "2025-09-12T07:14:50.499Z" }, + { url = "https://files.pythonhosted.org/packages/78/5b/da03983f19c7603cf3945d79df7e729070d10e6c1845b8f9d0ae70baac69/pikepdf-9.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c84358dce401f5bbb0725d38567fbd218de4e1efedd139b9626a8f9e3dc2cd66", size = 3794158, upload-time = "2025-09-12T07:14:52.801Z" }, + { url = "https://files.pythonhosted.org/packages/68/9d/254ebb94b7507e1c75000e2e638970af230265d068d1e42f2aad7023bcaf/pikepdf-9.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:94161a46a88fc7e1615985ba6839e0aadd801a61c105783b86ac3284279fdc83", size = 3833293, upload-time = "2025-09-12T07:14:54.711Z" }, ] [[package]] name = "pillow" -version = "10.0.1" +version = "12.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/9e/7e638579cce7dc346632f020914141a164a872be813481f058883ee8d421/Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d", size = 50528551, upload-time = "2023-09-15T14:00:45.533Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/34/4e02804420b1cd9371ce5f7c0da7024a8450e6b092c336d3233d6c6448d9/Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a", size = 3730082, upload-time = "2023-09-15T13:56:57.712Z" }, - { url = "https://files.pythonhosted.org/packages/55/93/9a8b06ca5917f246d1cd31351d5ec38651f8f5f9619c8db5751583f49ba3/Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d", size = 3281492, upload-time = "2023-09-15T13:57:03.209Z" }, - { url = "https://files.pythonhosted.org/packages/53/ac/3f9450af6583199b1c408f3a9a0f65e6dedc25aed15f691a74bcfa2d1077/Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d", size = 3411397, upload-time = "2023-09-15T13:57:07.418Z" }, - { url = "https://files.pythonhosted.org/packages/52/97/9fb1b66fbf98893722247fdf6ac39467a7b177483b546a94dbcff66653f0/Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19", size = 3486505, upload-time = "2023-09-15T13:57:10.787Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a7/35db8e4d15e8f139f9eda3378260a91b2ef7342342769dbeaff8dbc29c26/Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f", size = 3462862, upload-time = "2023-09-15T13:57:14.17Z" }, - { url = "https://files.pythonhosted.org/packages/7a/07/e896b096a77375e78e02ce222ae4fd6014928cd76c691d312060a1645dfa/Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff", size = 3607904, upload-time = "2023-09-15T13:57:18.109Z" }, - { url = "https://files.pythonhosted.org/packages/d2/4f/9bb94120b219e050588684cc5f4d1d48d97e0e4a1ec6c1aa68369132d2c7/Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf", size = 3548900, upload-time = "2023-09-15T13:57:21.438Z" }, - { url = "https://files.pythonhosted.org/packages/9e/33/0140c7dbe509d4e765558c976bea108fd2f63d527a9e16860392ccbcae72/Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd", size = 3587084, upload-time = "2023-09-15T13:57:25.339Z" }, - { url = "https://files.pythonhosted.org/packages/23/ca/7296d769f62266c0f94bf76496bc77114e7a96d2de3d7bcba91d0ba2856f/Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0", size = 2513767, upload-time = "2023-09-15T13:57:28.215Z" }, - { url = "https://files.pythonhosted.org/packages/2c/30/458e634572b833d8208ee83ca4e43bb6c21ccdd1b6149f4baf072f8807e8/Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db", size = 3684471, upload-time = "2023-09-15T13:59:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/af/66/d2aaf99819c7aef39f65b43c67ef968128aff33d44c230ec675b40a7ffb4/Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e", size = 3400133, upload-time = "2023-09-15T13:59:56.646Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ba/391fd1ff93e158cb34de3d2e003c070ee85b8840a4fe99a900463d75efad/Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4", size = 3489956, upload-time = "2023-09-15T14:00:00.343Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d0/069cb663f6b809668e4ed6620dc56d518f5bd30134458f783b5553d2ba67/Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f", size = 2513935, upload-time = "2023-09-15T14:00:04.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, +] + +[[package]] +name = "pip" +version = "26.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/15/4500e320e6b101ec3b719ae85b697d9940b6cda672bc555bd6016fc60c6f/pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f", size = 1848877, upload-time = "2026-08-04T22:51:14.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e", size = 1816632, upload-time = "2026-08-04T22:51:12.472Z" }, +] + +[[package]] +name = "pip-api" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, +] + +[[package]] +name = "pip-audit" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachecontrol", extra = ["filecache"] }, + { name = "cyclonedx-python-lib" }, + { name = "packaging" }, + { name = "pip-api" }, + { name = "pip-requirements-parser" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "rich" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, +] + +[[package]] +name = "pip-requirements-parser" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" }, ] [[package]] @@ -696,9 +856,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "prek" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/f1/8f3143530bf43fa82f8873f4e536f318b6143dd5c09171aba0d465e21f04/prek-0.5.4.tar.gz", hash = "sha256:7f5bc061880141e362ef422e52188d0090794c0622e442e3157686d1e342780a", size = 565182, upload-time = "2026-09-28T04:54:43.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/a7/f3a4a252c0846c5846dd86118ee0c1fddb7381dc35e3d44c2a5f3cea1757/prek-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:550ecaae0601f9405526c866a57cc6882ed1e9d8696ac1117b8703bb37d087eb", size = 6011113, upload-time = "2026-09-28T04:54:28.107Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e3/26638cf090b620e63e60a639b8e7d7eb38b9fbea74a0827a182fc922cf8e/prek-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7ef67cc23e23c6e9c6ca0b27a7b12c19f16774d36b1c88ed035f2639995718d", size = 5534338, upload-time = "2026-09-28T04:54:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/08/64b668eb088204650b96183cea739b56e6b179130c4398a8a83cbe9b125a/prek-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:59f2206850f643bafe46b28b4431525f8dcc5b7a763f271179bff10b8157870d", size = 5840937, upload-time = "2026-09-28T04:54:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ee/a0f847194eb6a2b67921b4c94c4fbf3f33a785affb190c6a2097344dbdd6/prek-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5b0cab1219a7bc9cc90785c7436f563765440616543ede7dfa3ba5c332cede", size = 6234354, upload-time = "2026-09-28T04:54:34.176Z" }, + { url = "https://files.pythonhosted.org/packages/32/9a/14186d90ed29b46650452068d6bd1ac233ca0ea41a3b35081c37f6b77b1c/prek-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8fc22db8132dab9aed4e152bf6a58a7f791f4c91c74305d65d96c48b8ec0bc62", size = 5845649, upload-time = "2026-09-28T04:54:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/e0/88/ff5bbbada4098f59740ab767f5c2f82d4110b90ea7db309a9bcf21f3dcb9/prek-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:19d4f3986dcd1dfa15f9417e32a75fb2ba084156dbd7ace161f0764281b8064b", size = 6351516, upload-time = "2026-09-28T04:54:38.539Z" }, + { url = "https://files.pythonhosted.org/packages/c8/1a/0cc410232ef42750b3bf34babd6def4a3943ec98259d9fecda3b26fcd63f/prek-0.5.4-py3-none-win_amd64.whl", hash = "sha256:3100233ba802eff6a4b09bc045c8f50d599bb9d810fa234823bc1642938b224f", size = 5755956, upload-time = "2026-09-28T04:54:40.227Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/3fd53cc54468db37462885d3139eae40bb469d51beae4575c986b190ab6e/prek-0.5.4-py3-none-win_arm64.whl", hash = "sha256:9fc4ad2d647f70a8a3a9900849e1dd89472cf5a71d830f083b62cf87a0406e27", size = 5502478, upload-time = "2026-09-28T04:54:41.973Z" }, +] + [[package]] name = "premailer" -version = "3.7.0" +version = "3.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -707,44 +883,56 @@ dependencies = [ { name = "lxml" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/7d/a3aff0b1083ecf0bc48e85e200135f5b0545814d01841013e2f884fd831e/premailer-3.7.0.tar.gz", hash = "sha256:5eec9603e84cee583a390de69c75192e50d76e38ef0292b027bd64923766aca7", size = 23377, upload-time = "2020-05-07T14:28:50.247Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/e49bd31941eff2987076383fa6d811eb785a28f498f5bb131e981bd71e13/premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2", size = 24342, upload-time = "2021-08-02T20:32:54.328Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/ce/74bbdf0eee4265fd3f161d4276b36c9238b802191c2053c8e68578bda4e6/premailer-3.7.0-py2.py3-none-any.whl", hash = "sha256:c7ac48986984a810afea5147bc8410a8fe0659bf52f357e78b28a1b949209b91", size = 18678, upload-time = "2020-05-07T14:28:48.927Z" }, -] - -[[package]] -name = "progress" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/26/3b086f0c5d6c1c18c2430d6fac3a99d79553884ca6cdf759cf256dd43b7d/progress-1.6.1.tar.gz", hash = "sha256:c1ba719f862ce885232a759eab47971fe74dfc7bb76ab8a51ef5940bad35086c", size = 7164, upload-time = "2025-07-01T05:50:43.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/59/123aee44a039b212cfb8d90be1adf06496a99b313ee1683aadf90b3d9799/progress-1.6.1-py3-none-any.whl", hash = "sha256:5239f22f305c12fdc8ce6e0e47f70f21622a935e16eafc4535617112e7c7ea0b", size = 9761, upload-time = "2025-07-01T05:50:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/b1/07/4e8d94f94c7d41ca5ddf8a9695ad87b888104e2fd41a35546c1dc9ca74ac/premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a", size = 19544, upload-time = "2021-08-02T20:32:52.771Z" }, ] [[package]] name = "psutil" -version = "5.8.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/b0/7276de53321c12981717490516b7e612364f2cb372ee8901bd4a66a000d7/psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6", size = 470886, upload-time = "2020-12-19T01:20:42.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2", size = 497660, upload-time = "2025-09-17T20:14:52.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/62/ce4051019ee20ce0ed74432dd73a5bb087a6704284a470bb8adff69a0932/psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13", size = 245242, upload-time = "2025-09-17T20:14:56.126Z" }, + { url = "https://files.pythonhosted.org/packages/38/61/f76959fba841bf5b61123fbf4b650886dc4094c6858008b5bf73d9057216/psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5", size = 246682, upload-time = "2025-09-17T20:14:58.25Z" }, + { url = "https://files.pythonhosted.org/packages/88/7a/37c99d2e77ec30d63398ffa6a660450b8a62517cabe44b3e9bae97696e8d/psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3", size = 287994, upload-time = "2025-09-17T20:14:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/9d/de/04c8c61232f7244aa0a4b9a9fbd63a89d5aeaf94b2fc9d1d16e2faa5cbb0/psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3", size = 291163, upload-time = "2025-09-17T20:15:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/58/c4f976234bf6d4737bc8c02a81192f045c307b72cf39c9e5c5a2d78927f6/psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d", size = 293625, upload-time = "2025-09-17T20:15:04.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca", size = 244812, upload-time = "2025-09-17T20:15:07.462Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d", size = 247965, upload-time = "2025-09-17T20:15:09.673Z" }, + { url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971, upload-time = "2025-09-17T20:15:12.262Z" }, +] [[package]] name = "psycopg2-binary" -version = "2.9.10" +version = "2.9.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/81/331257dbf2801cdb82105306042f7a1637cc752f65f2bb688188e0de5f0b/psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", size = 3043397, upload-time = "2024-10-16T11:18:58.647Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9a/7f4f2f031010bbfe6a02b4a15c01e12eb6b9b7b358ab33229f28baadbfc1/psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", size = 3274806, upload-time = "2024-10-16T11:19:03.935Z" }, - { url = "https://files.pythonhosted.org/packages/e5/57/8ddd4b374fa811a0b0a0f49b6abad1cde9cb34df73ea3348cc283fcd70b4/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", size = 2851361, upload-time = "2024-10-16T11:19:07.277Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/d1e52c20d283f1f3a8e7e5c1e06851d432f123ef57b13043b4f9b21ffa1f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", size = 3080836, upload-time = "2024-10-16T11:19:11.033Z" }, - { url = "https://files.pythonhosted.org/packages/a0/cb/592d44a9546aba78f8a1249021fe7c59d3afb8a0ba51434d6610cc3462b6/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", size = 3264552, upload-time = "2024-10-16T11:19:14.606Z" }, - { url = "https://files.pythonhosted.org/packages/64/33/c8548560b94b7617f203d7236d6cdf36fe1a5a3645600ada6efd79da946f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", size = 3019789, upload-time = "2024-10-16T11:19:18.889Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0e/c2da0db5bea88a3be52307f88b75eec72c4de62814cbe9ee600c29c06334/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", size = 2871776, upload-time = "2024-10-16T11:19:23.023Z" }, - { url = "https://files.pythonhosted.org/packages/15/d7/774afa1eadb787ddf41aab52d4c62785563e29949613c958955031408ae6/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", size = 2820959, upload-time = "2024-10-16T11:19:26.906Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ed/440dc3f5991a8c6172a1cde44850ead0e483a375277a1aef7cfcec00af07/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", size = 2919329, upload-time = "2024-10-16T11:19:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/03/be/2cc8f4282898306732d2ae7b7378ae14e8df3c1231b53579efa056aae887/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", size = 2957659, upload-time = "2024-10-16T11:19:32.864Z" }, - { url = "https://files.pythonhosted.org/packages/d0/12/fb8e4f485d98c570e00dad5800e9a2349cfe0f71a767c856857160d343a5/psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", size = 1024605, upload-time = "2024-10-16T11:19:35.462Z" }, - { url = "https://files.pythonhosted.org/packages/22/4f/217cd2471ecf45d82905dd09085e049af8de6cfdc008b6663c3226dc1c98/psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", size = 1163817, upload-time = "2024-10-16T11:19:37.384Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, +] + +[[package]] +name = "py-serializable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, ] [[package]] @@ -753,18 +941,17 @@ version = "1.28.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/40/d9/412da520de9052b7e80bfc810ec10f5cb3dbfa4aa3e23c2820dc61cdb3d0/pycairo-1.28.0.tar.gz", hash = "sha256:26ec5c6126781eb167089a123919f87baa2740da2cca9098be8b3a6b91cc5fbc", size = 662477, upload-time = "2025-04-14T20:11:08.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/67/6e5d6328c6037f0479017f51e97f5cbb79a68ed6e93f671dac17cb47bf2e/pycairo-1.28.0-cp310-cp310-win32.whl", hash = "sha256:53e6dbc98456f789965dad49ef89ce2c62f9a10fc96c8d084e14da0ffb73d8a6", size = 750438, upload-time = "2025-04-14T20:10:43.722Z" }, - { url = "https://files.pythonhosted.org/packages/00/79/e941186c2275333643504f57711b157ba17e81a2be805346585ad7fd382e/pycairo-1.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:c8ab91a75025f984bc327ada335c787efb61c929ea0512063793cb36cee503d4", size = 841526, upload-time = "2025-04-14T20:10:45.557Z" }, - { url = "https://files.pythonhosted.org/packages/84/4b/3495baabd3c830bf80bf112a0e645342bfe8f3fc05ed6423444adf62d496/pycairo-1.28.0-cp310-cp310-win_arm64.whl", hash = "sha256:e955328c1a5147bf71ee94e206413ce15e12630296a79788fcd246c80e5337b8", size = 691866, upload-time = "2025-04-16T19:30:17.73Z" }, + { url = "https://files.pythonhosted.org/packages/96/1b/1f5da2b2e44b33a5b269ff28af710b0a7903001d9cf8a40d00fc944a5092/pycairo-1.28.0-cp314-cp314-win32.whl", hash = "sha256:d0ab30585f536101ad6f09052fc3895e2a437ba57531ea07223d0e076248025d", size = 766699, upload-time = "2025-07-24T06:36:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/c9/91/1d5f18bd3ed469b1de8f83bfbf8bd67d23439f075151b66740fd35356190/pycairo-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:94f2ed204999ab95a0671a0fa948ffbb9f3d6fb8731fe787917f6d022d9c1c0f", size = 868725, upload-time = "2025-07-24T06:36:09.597Z" }, ] [[package]] name = "pycodestyle" -version = "2.9.1" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", size = 102127, upload-time = "2022-08-03T23:13:29.715Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b", size = 41493, upload-time = "2022-08-03T23:13:27.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, ] [[package]] @@ -778,49 +965,29 @@ wheels = [ [[package]] name = "pygments" -version = "2.15.1" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/6b/2114e54b290824197006e41be3f9bbe1a26e9c39d1f5fa20a6d62945a0b3/Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c", size = 4819591, upload-time = "2023-04-18T18:49:24.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/a7/37c8d68532ba71549db4212cb036dbd6161b40e463aba336770e80c72f84/Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1", size = 1146422, upload-time = "2023-04-18T18:49:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c1/47/dfc9c342c9842bbe0036c7f763d2d6686bcf5eb1808ba3e170afdb282210/pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", size = 649718, upload-time = "2020-04-05T22:21:25.349Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b", size = 67842, upload-time = "2020-04-05T22:21:22.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] -name = "pypdf2" -version = "1.27.12" +name = "pypdf" +version = "6.19.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/ff/d27c15e86f7b39b53d4f33fec4d395cc3b82bd442f863cbe5c5c60ecdb8e/PyPDF2-1.27.12.tar.gz", hash = "sha256:20929fad10a3b4890862f65f3a46f563cfdf53132faae5193b54e18658467a60", size = 2703058, upload-time = "2022-05-02T19:10:40.722Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ac/63d71aaedb59acbcdef491e6ca6469165e3771c9c74358204818fd9bc5a6/pypdf-6.19.0.tar.gz", hash = "sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155", size = 7033266, upload-time = "2026-09-16T09:32:05.946Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/97/1501eaf50a50e30343760df715e0db0a15cbcfd6e13840318d159490477e/PyPDF2-1.27.12-py3-none-any.whl", hash = "sha256:9416c347b4c03391caf7562486bec0fd129bbb6a3359eefe4a0b758d0e3dc20c", size = 80030, upload-time = "2022-05-02T19:10:36.217Z" }, -] - -[[package]] -name = "pypom" -version = "2.2.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pluggy" }, - { name = "selenium" }, - { name = "zope-component" }, - { name = "zope-interface" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/3e/fc2061ee47b99c3bf7f41d13107276f5093211b958cf67654a9dd0d44684/PyPOM-2.2.4.tar.gz", hash = "sha256:5da52cf447e62f43a0cfa47dfe52eb822eff07b2fdad759f930d1d227c15220b", size = 12665, upload-time = "2022-09-27T09:29:18.267Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/79/bd7fedaadaaa70c64319bda9d2b3c0584524f13f51707411f08b966a0951/PyPOM-2.2.4-py2.py3-none-any.whl", hash = "sha256:8b4dc6d1a24580298bf5ad8ad6c586f33b73c326c10a4419f83aee1abb20077d", size = 17028, upload-time = "2022-09-27T09:29:16.063Z" }, -] - -[package.optional-dependencies] -splinter = [ - { name = "splinter" }, + { url = "https://files.pythonhosted.org/packages/3c/2c/c43c03eaf630435f023f1dc61ec4a4a78951ad5530a62c71cc89bde307b7/pypdf-6.19.0-py3-none-any.whl", hash = "sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14", size = 395480, upload-time = "2026-09-16T09:32:04.087Z" }, ] [[package]] @@ -828,190 +995,94 @@ name = "pyrigs" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "ansicolors" }, - { name = "asgiref" }, - { name = "backports-tempfile" }, - { name = "backports-weakref" }, { name = "beautifulsoup4" }, { name = "brotli" }, - { name = "cachetools" }, - { name = "chardet" }, - { name = "configparser" }, - { name = "contextlib2" }, - { name = "cssselect" }, - { name = "cssutils" }, { name = "diff-match-patch" }, - { name = "dj-database-url" }, - { name = "dj-static" }, { name = "django" }, - { name = "django-debug-toolbar" }, - { name = "django-filter" }, { name = "django-hcaptcha" }, { name = "django-htmlmin" }, { name = "django-ical" }, { name = "django-mass-edit" }, { name = "django-queryable-properties" }, - { name = "django-recurrence" }, { name = "django-registration-redux" }, { name = "django-reversion" }, { name = "django-widget-tweaks" }, { name = "envparse" }, { name = "gunicorn" }, - { name = "icalendar" }, - { name = "idna" }, - { name = "importlib-metadata" }, { name = "markdown" }, - { name = "msgpack" }, - { name = "pep517" }, - { name = "pikepdf" }, { name = "pillow" }, { name = "premailer" }, - { name = "progress" }, - { name = "psutil" }, { name = "psycopg2-binary" }, - { name = "pygments" }, - { name = "pyparsing" }, - { name = "pypdf2" }, - { name = "pypom" }, + { name = "pypdf" }, { name = "python-barcode" }, - { name = "python-dateutil" }, - { name = "pytoml" }, { name = "pytz" }, { name = "reportlab" }, - { name = "requests" }, - { name = "retrying" }, - { name = "selenium" }, { name = "sentry-sdk" }, { name = "simplejson" }, - { name = "six" }, - { name = "soupsieve" }, - { name = "sqlparse" }, - { name = "static3" }, - { name = "svg2rlg" }, - { name = "tini" }, - { name = "tornado" }, - { name = "urllib3" }, { name = "whitenoise" }, - { name = "yolk" }, { name = "z3c-rml" }, - { name = "zipp" }, - { name = "zope-component" }, - { name = "zope-deferredimport" }, - { name = "zope-deprecation" }, - { name = "zope-event" }, - { name = "zope-hookable" }, - { name = "zope-interface" }, - { name = "zope-proxy" }, - { name = "zope-schema" }, ] [package.dev-dependencies] dev = [ + { name = "autopep8" }, { name = "coveralls" }, { name = "django-coverage-plugin" }, - { name = "pluggy" }, - { name = "pycodestyle" }, - { name = "pypom", extra = ["splinter"] }, + { name = "pip-audit" }, + { name = "prek" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-django" }, { name = "pytest-reverse" }, - { name = "pytest-splinter" }, { name = "pytest-xdist", extra = ["psutil"] }, + { name = "ruff" }, + { name = "selenium" }, ] [package.metadata] requires-dist = [ - { name = "ansicolors", specifier = "~=1.1.8" }, - { name = "asgiref", specifier = "~=3.3.1" }, - { name = "backports-tempfile", specifier = "~=1.0" }, - { name = "backports-weakref", specifier = "~=1.0.post1" }, - { name = "beautifulsoup4", specifier = "~=4.9.3" }, - { name = "brotli", specifier = "~=1.0.9" }, - { name = "cachetools", specifier = "~=4.2.1" }, - { name = "chardet", specifier = "~=4.0.0" }, - { name = "configparser", specifier = "~=5.0.1" }, - { name = "contextlib2", specifier = "~=0.6.0.post1" }, - { name = "cssselect", specifier = "~=1.1.0" }, - { name = "cssutils", specifier = "~=1.0.2" }, - { name = "diff-match-patch" }, - { name = "dj-database-url", specifier = "~=0.5.0" }, - { name = "dj-static", specifier = "~=0.0.6" }, - { name = "django", specifier = "~=3.2" }, - { name = "django-debug-toolbar", specifier = "~=4.0.0" }, - { name = "django-filter", specifier = "~=2.4.0" }, - { name = "django-hcaptcha" }, - { name = "django-htmlmin", specifier = "~=0.11.0" }, - { name = "django-ical", specifier = "~=1.7.1" }, - { name = "django-mass-edit" }, - { name = "django-queryable-properties" }, - { name = "django-recurrence", specifier = "~=1.10.3" }, - { name = "django-registration-redux", specifier = "~=2.9" }, - { name = "django-reversion", specifier = "~=3.0.9" }, - { name = "django-widget-tweaks", specifier = "~=1.4.8" }, - { name = "envparse" }, - { name = "gunicorn", specifier = "~=22.0.0" }, - { name = "icalendar", specifier = "~=4.0.7" }, - { name = "idna", specifier = "~=3.7" }, - { name = "importlib-metadata" }, - { name = "markdown", specifier = "~=3.3.3" }, - { name = "msgpack", specifier = "~=1.0.2" }, - { name = "pep517", specifier = "~=0.9.1" }, - { name = "pikepdf" }, - { name = "pillow", specifier = "~=10.0.1" }, - { name = "premailer", specifier = "~=3.7.0" }, - { name = "progress", specifier = "~=1.5" }, - { name = "psutil", specifier = "~=5.8.0" }, - { name = "psycopg2-binary" }, - { name = "pygments", specifier = "~=2.15.0" }, - { name = "pyparsing", specifier = "~=2.4.7" }, - { name = "pypdf2", specifier = "~=1.27.5" }, - { name = "pypom", specifier = "~=2.2.4" }, - { name = "python-barcode" }, - { name = "python-dateutil", specifier = "~=2.8.1" }, - { name = "pytoml", specifier = "~=0.1.21" }, - { name = "pytz", specifier = "~=2020.5" }, - { name = "reportlab" }, - { name = "requests", specifier = "~=2.32.3" }, - { name = "retrying", specifier = "~=1.3.3" }, - { name = "selenium", specifier = "~=4.9.1" }, - { name = "sentry-sdk" }, - { name = "simplejson", specifier = "~=3.17.2" }, - { name = "six", specifier = "~=1.15.0" }, - { name = "soupsieve", specifier = "~=2.1" }, - { name = "sqlparse", specifier = "~=0.5.0" }, - { name = "static3", specifier = "~=0.7.0" }, - { name = "svg2rlg", specifier = "~=0.3" }, - { name = "tini", specifier = "~=3.0.1" }, - { name = "tornado", specifier = "~=6.3" }, - { name = "urllib3", specifier = "~=1.26.19" }, - { name = "whitenoise", specifier = "~=5.2.0" }, - { name = "yolk", specifier = "~=0.4.3" }, - { name = "z3c-rml" }, - { name = "zipp", specifier = "~=3.4.0" }, - { name = "zope-component", specifier = "~=4.6.2" }, - { name = "zope-deferredimport", specifier = "~=4.3.1" }, - { name = "zope-deprecation", specifier = "~=4.4.0" }, - { name = "zope-event", specifier = "~=4.5.0" }, - { name = "zope-hookable", specifier = "~=5.0.1" }, - { name = "zope-interface" }, - { name = "zope-proxy", specifier = "~=4.3.5" }, - { name = "zope-schema", specifier = "~=6.0.1" }, + { name = "beautifulsoup4", specifier = "~=4.14" }, + { name = "brotli", specifier = "~=1.2" }, + { name = "diff-match-patch", specifier = ">=20241021,<20300000" }, + { name = "django", specifier = "~=5.2" }, + { name = "django-hcaptcha", specifier = "~=0.2" }, + { name = "django-htmlmin", specifier = "~=0.11" }, + { name = "django-ical", specifier = "~=1.9" }, + { name = "django-mass-edit", specifier = "~=3.5" }, + { name = "django-queryable-properties", specifier = "~=1.12" }, + { name = "django-registration-redux", specifier = "~=2.13" }, + { name = "django-reversion", specifier = "~=6.0" }, + { name = "django-widget-tweaks", specifier = "~=1.5" }, + { name = "envparse", specifier = "~=0.2" }, + { name = "gunicorn", specifier = "~=26.1" }, + { name = "markdown", specifier = "~=3.9" }, + { name = "pillow", specifier = "~=12.3" }, + { name = "premailer", specifier = "~=3.10" }, + { name = "psycopg2-binary", specifier = "~=2.9" }, + { name = "pypdf", specifier = "~=6.19" }, + { name = "python-barcode", specifier = "~=0.16" }, + { name = "pytz", specifier = "~=2025.2" }, + { name = "reportlab", specifier = "~=4.4" }, + { name = "sentry-sdk", specifier = "~=2.68" }, + { name = "simplejson", specifier = "~=3.20" }, + { name = "whitenoise", specifier = "~=6.12" }, + { name = "z3c-rml", specifier = "~=5.0" }, ] [package.metadata.requires-dev] dev = [ - { name = "coveralls" }, - { name = "django-coverage-plugin" }, - { name = "pluggy" }, - { name = "pycodestyle", specifier = "~=2.9.1" }, - { name = "pypom", extras = ["splinter"] }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, - { name = "pytest-reverse" }, - { name = "pytest-splinter" }, - { name = "pytest-xdist", extras = ["psutil"] }, + { name = "autopep8", specifier = "~=2.3" }, + { name = "coveralls", specifier = "~=4.0" }, + { name = "django-coverage-plugin", specifier = "~=3.1" }, + { name = "pip-audit", specifier = "~=2.10" }, + { name = "prek", specifier = "~=0.5" }, + { name = "pytest", specifier = "~=9.1" }, + { name = "pytest-cov", specifier = "~=7.1" }, + { name = "pytest-django", specifier = "~=4.14" }, + { name = "pytest-reverse", specifier = "~=1.9" }, + { name = "pytest-xdist", extras = ["psutil"], specifier = "~=3.8" }, + { name = "ruff", specifier = "~=0.16" }, + { name = "selenium", specifier = "~=4.35" }, ] [[package]] @@ -1025,46 +1096,44 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] name = "pytest-django" -version = "4.11.1" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/f6/3851312120c2bf2f19cafff931e75059aad1ba670703cd751e2fde9bc942/pytest_django-4.14.0.tar.gz", hash = "sha256:26787dd3f422cfbab8f55b80a776e2edea7a11092cb74e960bef1312515708ef", size = 94700, upload-time = "2026-08-10T14:13:08.319Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/9c/03/850bffad2b581c440ca51c039d74504d5a422c94bda0bdb8a8ba5068d48b/pytest_django-4.14.0-py3-none-any.whl", hash = "sha256:c533b08d89cc675efcd5398eea270b34547e35f9a3608e2c9748dd88428ea187", size = 27067, upload-time = "2026-08-10T14:13:06.998Z" }, ] [[package]] @@ -1079,22 +1148,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/1c/126e8d934e8852c89fb898f77512050c0c34cf96f195f02cbd36c5dd0356/pytest_reverse-1.9.0-py3-none-any.whl", hash = "sha256:3cb9cb2403eea2f953fd6b9629637387cf6e75594f8573738f68ec1aca11dcd1", size = 4156, upload-time = "2025-09-09T10:39:00.744Z" }, ] -[[package]] -name = "pytest-splinter" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, - { name = "selenium" }, - { name = "setuptools" }, - { name = "splinter" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/34/ae72995e7daf734cfcab777108c8ef0c5a83f07314acfd7754f59033c7af/pytest-splinter-3.3.2.tar.gz", hash = "sha256:8a2aadab4be040a763865ac33e37a118194ddba746c6a91183d728ff65f63308", size = 24124, upload-time = "2022-09-09T19:46:54.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/9c/5721cf540d8e48c6721b0679eaa853048a7796ac03c4cec7c430cd92cbf8/pytest_splinter-3.3.2-py3-none-any.whl", hash = "sha256:8ec3094e42c70c7b4aa27834918cfa96e4c0d453c8e7a3055777bc061f976244", size = 18292, upload-time = "2022-09-09T19:46:51.419Z" }, -] - [[package]] name = "pytest-xdist" version = "3.8.0" @@ -1124,32 +1177,23 @@ wheels = [ [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/c4/13b4776ea2d76c115c1d1b84579f3764ee6d57204f6be27119f13a61d0a9/python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", size = 357324, upload-time = "2021-07-14T08:19:19.783Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/7a/87837f39d0296e723bb9b62bbb257d0355c7f6128853c78955f57342a56d/python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9", size = 247702, upload-time = "2021-07-14T08:19:18.161Z" }, -] - -[[package]] -name = "pytoml" -version = "0.1.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/ba/98ee2054a2d7b8bebd367d442e089489250b6dc2aee558b000e961467212/pytoml-0.1.21.tar.gz", hash = "sha256:8eecf7c8d0adcff3b375b09fe403407aa9b645c499e5ab8cac670ac4a35f61e7", size = 8783, upload-time = "2019-07-21T17:58:13.306Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/47/c7f8a0f210ad18576840922e0b504f0b7f5f73aea4a52ab14c5b58517edf/pytoml-0.1.21-py2.py3-none-any.whl", hash = "sha256:57a21e6347049f73bfb62011ff34cd72774c031b9828cb628a752225136dfc33", size = 8537, upload-time = "2019-07-21T17:58:12.186Z" }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pytz" -version = "2020.5" +version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/44/404ec10dca553032900a65bcded8b8280cf7c64cc3b723324e2181bf93c9/pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5", size = 314194, upload-time = "2020-12-24T20:58:07.498Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/06/2c2d3034b4d6bf22f2a4ae546d16925898658a33b4400cfb7e2c1e2871a3/pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", size = 510773, upload-time = "2020-12-24T20:58:04.098Z" }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] @@ -1167,7 +1211,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1175,18 +1219,22 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] -name = "retrying" -version = "1.3.7" +name = "rich" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/4b/8e06a67c8256a546107d2287bf0b0053a0f147de9a0763b10f12d7717eca/retrying-1.3.7.tar.gz", hash = "sha256:7aab7324db8ad92029eef1073bf7257315cc7d9f861243a66b916c164a2c3c20", size = 11171, upload-time = "2025-07-05T05:00:21.385Z" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/41/aeac1881a65fc3e6326ca3cbd6e39a37c78ed55f6fb4d26203bddd37802f/retrying-1.3.7-py3-none-any.whl", hash = "sha256:4449b0a9f6754ab381aabc2771a2aa8350cfe194c2238992e1303b662cfa94e2", size = 11965, upload-time = "2025-07-05T05:00:20.078Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -1202,69 +1250,86 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/00/d26b61e82163a59334e9f2ae31ba95ac01922da3792f090e11762d4422e8/rlpycairo-0.4.0-py3-none-any.whl", hash = "sha256:3ce83825d5761c03bc3571c7db12a336ad51417e63189e3512d11b8922576aa9", size = 7558, upload-time = "2025-08-15T12:24:52.544Z" }, ] +[[package]] +name = "ruff" +version = "0.16.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, +] + [[package]] name = "selenium" -version = "4.9.1" +version = "4.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "trio" }, { name = "trio-websocket" }, + { name = "typing-extensions" }, { name = "urllib3", extra = ["socks"] }, + { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/e2/0e5bee6762a7bf7852b47a79c5b12f9e526e6962958dbb9719fa490ba24c/selenium-4.9.1.tar.gz", hash = "sha256:3444f4376321530c36ce8355b6b357d8cf4a7d588ce5cf772183465930bbed0e", size = 6341857, upload-time = "2023-05-08T12:05:29.894Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/67/9016942b5781843cfea6f5bc1383cea852d9fa08f85f55a0547874525b5c/selenium-4.35.0.tar.gz", hash = "sha256:83937a538afb40ef01e384c1405c0863fa184c26c759d34a1ebbe7b925d3481c", size = 907991, upload-time = "2025-08-12T15:46:40.822Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/5c/803b797ba26bcd741c3b460b9a2ac663c04f21e8e4e425d26c1c842ca438/selenium-4.9.1-py3-none-any.whl", hash = "sha256:82aedaa85d55bc861f4c89ff9609e82f6c958e2e1e3da3ffcc36703f21d3ee16", size = 6551247, upload-time = "2023-05-08T12:05:22.544Z" }, + { url = "https://files.pythonhosted.org/packages/17/ef/d0e033e1b3f19a0325ce03863b68d709780908381135fc0f9436dea76a7b/selenium-4.35.0-py3-none-any.whl", hash = "sha256:90bb6c6091fa55805785cf1660fa1e2176220475ccdb466190f654ef8eef6114", size = 9602106, upload-time = "2025-08-12T15:46:38.244Z" }, ] [[package]] name = "sentry-sdk" -version = "2.39.0" +version = "2.68.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/72/43294fa4bdd75c51610b5104a3ff834459ba653abb415150aa7826a249dd/sentry_sdk-2.39.0.tar.gz", hash = "sha256:8c185854d111f47f329ab6bc35993f28f7a6b7114db64aa426b326998cfa14e9", size = 348556, upload-time = "2025-09-25T09:15:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/94/23b7dd072acb9628907bd3f4fbf61794a7b12a9db8f33c1276f70ae5ac92/sentry_sdk-2.68.0.tar.gz", hash = "sha256:648c58e9887311a03470a41539e24bdbbf64a30ca4f5336f7e3dcc87276400b3", size = 1008854, upload-time = "2026-08-13T09:06:21.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/44/4356cc64246ba7b2b920f7c97a85c3c52748e213e250b512ee8152eb559d/sentry_sdk-2.39.0-py2.py3-none-any.whl", hash = "sha256:ba655ca5e57b41569b18e2a5552cb3375209760a5d332cdd87c6c3f28f729602", size = 370851, upload-time = "2025-09-25T09:15:36.35Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9b/e2421d08956d0bc4691d995393d835e563886bff499d8fb10fdefae85a8d/sentry_sdk-2.68.0-py3-none-any.whl", hash = "sha256:538e56c2d03679d42f7c0cb5f1af73a7a510b00abc7e296c13ac49b107b713a4", size = 518670, upload-time = "2026-08-13T09:06:19.735Z" }, ] [[package]] name = "setuptools" -version = "80.9.0" +version = "84.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, ] [[package]] name = "simplejson" -version = "3.17.6" +version = "3.20.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/47/c7cc3d4ed15f09917838a2fb4e1759eafb6d2f37ebf7043af984d8b36cf7/simplejson-3.17.6.tar.gz", hash = "sha256:cf98038d2abf63a1ada5730e91e84c642ba6c225b0198c3684151b1f80c5f8a6", size = 83134, upload-time = "2021-11-16T16:37:32.318Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/58/71da798bdda7a7e3bf3223e0d407cc8c9c368d918e0e8a3459670ffc6392/simplejson-3.17.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd16302d39c4d6f4afde80edd0c97d4db643327d355a312762ccd9bd2ca515ed", size = 91620, upload-time = "2021-11-16T16:36:39.456Z" }, - { url = "https://files.pythonhosted.org/packages/3d/01/5eadfad8dd2e1cc5c8d49c83091121220cdf60cd230b60a274834852e484/simplejson-3.17.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:deac4bdafa19bbb89edfb73b19f7f69a52d0b5bd3bb0c4ad404c1bbfd7b4b7fd", size = 74290, upload-time = "2021-11-16T16:36:40.96Z" }, - { url = "https://files.pythonhosted.org/packages/a4/47/4329a856f2e7c7d015738271832328aa56da1bef11c5bdbdfe65d9fb51ba/simplejson-3.17.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8bbdb166e2fb816e43ab034c865147edafe28e1b19c72433147789ac83e2dda", size = 73652, upload-time = "2021-11-16T16:36:42.302Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/2ab6ccb7d83098ac44ca820a731ba980db5d03dc3f52cab1275d3f4755c4/simplejson-3.17.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7854326920d41c3b5d468154318fe6ba4390cb2410480976787c640707e0180", size = 131264, upload-time = "2021-11-16T16:36:43.138Z" }, - { url = "https://files.pythonhosted.org/packages/26/79/3659838553d2f8733624a0c27a3bff0d375c4b9beec159dc019361f4191d/simplejson-3.17.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:04e31fa6ac8e326480703fb6ded1488bfa6f1d3f760d32e29dbf66d0838982ce", size = 135301, upload-time = "2021-11-16T16:36:44.979Z" }, - { url = "https://files.pythonhosted.org/packages/94/6a/d815b0b2cbc11611d37e75bbec2abddd2141a9de711e9bf4a7af32d192e9/simplejson-3.17.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f63600ec06982cdf480899026f4fda622776f5fabed9a869fdb32d72bc17e99a", size = 137093, upload-time = "2021-11-16T16:36:45.923Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f1/818adb18efca86480fb0024101db564ae741d15e3bfc567be14fc8c227a9/simplejson-3.17.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e03c3b8cc7883a54c3f34a6a135c4a17bc9088a33f36796acdb47162791b02f6", size = 135521, upload-time = "2021-11-16T16:36:46.827Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/292bd32c3186841803c484420fedccbe16229f28a99ad34ee42461a640f0/simplejson-3.17.6-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a2d30d6c1652140181dc6861f564449ad71a45e4f165a6868c27d36745b65d40", size = 134053, upload-time = "2021-11-16T16:36:47.659Z" }, - { url = "https://files.pythonhosted.org/packages/b4/03/34f2db52c3ff48dd9acf4a603fd8110ed60ec966a330c5e52307630e2f41/simplejson-3.17.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a1aa6e4cae8e3b8d5321be4f51c5ce77188faf7baa9fe1e78611f93a8eed2882", size = 136614, upload-time = "2021-11-16T16:36:48.579Z" }, - { url = "https://files.pythonhosted.org/packages/d0/45/3e1ce2a732663b380fc2ee9fb89667ac0000fd94215e6d7c869ca968160c/simplejson-3.17.6-cp310-cp310-win32.whl", hash = "sha256:97202f939c3ff341fc3fa84d15db86156b1edc669424ba20b0a1fcd4a796a045", size = 73796, upload-time = "2021-11-16T16:36:49.612Z" }, - { url = "https://files.pythonhosted.org/packages/59/94/2fba254afb6808db7fb95ba4727bb1f329021660ba9a2f2f51f59207e263/simplejson-3.17.6-cp310-cp310-win_amd64.whl", hash = "sha256:80d3bc9944be1d73e5b1726c3bbfd2628d3d7fe2880711b1eb90b617b9b8ac70", size = 75876, upload-time = "2021-11-16T16:36:50.873Z" }, + { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, ] [[package]] name = "six" -version = "1.15.0" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/34/415834bfdafca3c5f451532e8a8d9ba89a21c9743a0c59fbd0205c7f9426/six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", size = 33917, upload-time = "2020-05-21T15:25:55.142Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/ff/48bde5c0f013094d729fe4b0316ba2a24774b3ff1c52d924a8a4cb04078a/six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced", size = 10963, upload-time = "2020-05-21T15:25:54.177Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] @@ -1287,46 +1352,22 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8" +version = "2.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, -] - -[[package]] -name = "splinter" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/7f/afb1788635b3532dfb9e2f21698ce8825e99714750ae4bf30bbc3ecaa9d5/splinter-0.21.0.tar.gz", hash = "sha256:60cbbec9209acefccf39608d677eecdb248bbd700023663ce406595322c38fe5", size = 30896, upload-time = "2024-01-16T00:59:04.874Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/67/ecd32a6a634e10a222aabe2b0a15b19857013ec8b1150cc664df79c025fa/splinter-0.21.0-py3-none-any.whl", hash = "sha256:4c9e5962ce25f468b2dd52effbd1445afd7466740f238e564c6b70f7fda9368f", size = 40812, upload-time = "2024-01-16T00:59:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" }, ] [[package]] name = "sqlparse" -version = "0.5.3" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" }, + { url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" }, ] -[[package]] -name = "static3" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/b0/9cf15108b73c4f2ffffe11d237c938f57785f55d3693d822d565432cb680/static3-0.7.0.tar.gz", hash = "sha256:674641c64bc75507af2eb20bef7e7e3593dca993dec6674be108fa15b42f47c8", size = 24607, upload-time = "2016-05-10T11:43:45.17Z" } - -[[package]] -name = "svg2rlg" -version = "0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/b3/dfa71a4627462789ade6fa3b32396be263ad043556f3ee133030d6c8d05d/svg2rlg-0.3.tar.gz", hash = "sha256:05db4480b90e912e08727d4cb24385fe33e8436def079b8f149b61a350638bee", size = 490908, upload-time = "2010-11-03T12:20:02.114Z" } - [[package]] name = "svglib" version = "1.6.0" @@ -1343,19 +1384,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/93/01273a1b8d8454d45f2e18b3d6098c7be13a0864a55fbd0ebda7815c201a/svglib-1.6.0-py3-none-any.whl", hash = "sha256:9aea8e2e81cbbf9c844460e4c7dc90e0a06aea7983bc201975ccd279d7b2d194", size = 39163, upload-time = "2025-09-25T09:48:35.927Z" }, ] -[[package]] -name = "tini" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "configparser" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/9f/c62f805b94613c9ea1bb16af6b7f7a1fe2baded5eec0c6ab4861e2851f93/tini-3.0.1.tar.gz", hash = "sha256:f856780a90e7a3cdf3aaea56cb8c176fc5f4f8e1d126362cb88ca104f835d99f", size = 3033, upload-time = "2015-05-06T18:46:48.429Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/12/db6dc903040bf7e3b86514bc4352d1aab1432db09a27bd36251bd000d2fc/tini-3.0.1-py2.py3-none-any.whl", hash = "sha256:8062be51e6766c15ec402579fff422d8c2fb46bdb3f0b22bd009f32d8dd79c81", size = 4334, upload-time = "2015-05-06T18:46:44.501Z" }, -] - [[package]] name = "tinycss2" version = "1.4.0" @@ -1368,59 +1396,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, -] - [[package]] name = "tomli" -version = "2.2.1" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] -name = "tornado" -version = "6.5.2" +name = "tomli-w" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, - { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, - { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, - { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, - { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] [[package]] name = "trio" -version = "0.31.0" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, - { name = "exceptiongroup" }, { name = "idna" }, { name = "outcome" }, { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/8f/c6e36dd11201e2a565977d8b13f0b027ba4593c1a80bed5185489178e257/trio-0.31.0.tar.gz", hash = "sha256:f71d551ccaa79d0cb73017a33ef3264fde8335728eb4c6391451fe5d253a9d5b", size = 605825, upload-time = "2025-09-09T15:17:15.242Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776, upload-time = "2025-04-21T00:48:19.507Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/5b/94237a3485620dbff9741df02ff6d8acaa5fdec67d81ab3f62e4d8511bf7/trio-0.31.0-py3-none-any.whl", hash = "sha256:b5d14cd6293d79298b49c3485ffd9c07e3ce03a6da8c7dfbe0cb3dd7dc9a4774", size = 512679, upload-time = "2025-09-09T15:17:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194, upload-time = "2025-04-21T00:48:17.167Z" }, ] [[package]] @@ -1428,7 +1454,6 @@ name = "trio-websocket" version = "0.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup" }, { name = "outcome" }, { name = "trio" }, { name = "wsproto" }, @@ -1440,20 +1465,29 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] [[package]] name = "urllib3" -version = "1.26.20" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [package.optional-dependencies] @@ -1471,12 +1505,21 @@ wheels = [ ] [[package]] -name = "whitenoise" -version = "5.2.0" +name = "websocket-client" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/8a/cd6346ffd78f5ff9aa9ce750f3e8d75cde7c60fbe197ac10bdea49d61cff/whitenoise-5.2.0.tar.gz", hash = "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7", size = 45096, upload-time = "2020-08-04T09:33:38.557Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/83/5d91949e370e52578a99ef6391c3b3e19f9fd1f5b4f58d5cbd6e2862d4a8/whitenoise-5.2.0-py2.py3-none-any.whl", hash = "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d", size = 19775, upload-time = "2020-08-04T09:33:41.561Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + +[[package]] +name = "whitenoise" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, ] [[package]] @@ -1485,16 +1528,26 @@ version = "1.17.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] @@ -1510,15 +1563,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, ] -[[package]] -name = "yolk" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/c0/73510e50668b70f3fad25328c58d7a29a300a2e452058dfdcb7c7538af7b/yolk-0.4.3.tar.gz", hash = "sha256:1c07eb4001dc133c08e66e38c5d58faa7616ae804f8d0ab02dd44a1044e7ddb8", size = 86100, upload-time = "2012-02-06T21:36:11.709Z" } - [[package]] name = "z3c-rml" version = "5.0" @@ -1540,112 +1584,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/91/8cb966dd5a122df358da78929c88046ddc93880663dbd37ec226cb393286/z3c_rml-5.0-py3-none-any.whl", hash = "sha256:04260368f97059ce29cfc9315be34ea31360d36427812381f8623edf7f87dd2a", size = 1967221, upload-time = "2025-04-14T06:50:30.282Z" }, ] -[[package]] -name = "zipp" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/0b/396f6fa399c95b01e3040a2c0907f777defc75a502f4dcd8441530e3d6a4/zipp-3.4.2.tar.gz", hash = "sha256:ce85de43ee0ead77dd0fbee3902bec1501aef59b92a2e18265396b22a1d756ab", size = 12581, upload-time = "2021-07-02T22:56:57.178Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/1c/b361860b2ef2a34c12e3c3244411f1d3beaf8c534a3e85ab7d6d61c7ee51/zipp-3.4.2-py3-none-any.whl", hash = "sha256:a5303f8ad20aff64720bf548256646f74fc8c167065c0d177a98a7cadceed85a", size = 5275, upload-time = "2021-07-02T22:56:56.315Z" }, -] - -[[package]] -name = "zope-component" -version = "4.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, - { name = "zope-deferredimport" }, - { name = "zope-deprecation" }, - { name = "zope-event" }, - { name = "zope-hookable" }, - { name = "zope-interface" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/e7/14935f56295e6a9a4ebcc82e30403f4bf580760801ea7ac6bae61ef0267b/zope.component-4.6.2.tar.gz", hash = "sha256:91628918218b3e6f6323de2a7b845e09ddc5cae131c034896c051b084bba3c92", size = 90951, upload-time = "2020-07-03T15:25:08.641Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/dd/802e78f6c10f960ebcce8614092a098d4c668afd5564b45fc28bbd042918/zope.component-4.6.2-py2.py3-none-any.whl", hash = "sha256:607628e4c84f7887a69a958542b5c304663e726b73aba0882e3a3f059bff14f3", size = 67427, upload-time = "2020-07-03T15:25:06.718Z" }, -] - -[[package]] -name = "zope-deferredimport" -version = "4.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, - { name = "zope-proxy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/74/6eb2dcf013fac35d086abef2435b5a6621435c2b0c166ef5b63a1b51e91d/zope.deferredimport-4.3.1.tar.gz", hash = "sha256:57b2345e7b5eef47efcd4f634ff16c93e4265de3dcf325afc7315ade48d909e1", size = 20244, upload-time = "2019-08-05T06:11:56.506Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/e3/05b02057b56cd9c59d848b67aff1cc701e1d2237055ebd0d0c1f44331186/zope.deferredimport-4.3.1-py2.py3-none-any.whl", hash = "sha256:9a0c211df44aa95f1c4e6d2626f90b400f56989180d3ef96032d708da3d23e0a", size = 10418, upload-time = "2019-08-05T06:11:54.377Z" }, -] - -[[package]] -name = "zope-deprecation" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/da/46e92d32d545dd067b9436279d84c339e8b16de2ca393d7b892bc1e1e9fd/zope.deprecation-4.4.0.tar.gz", hash = "sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df", size = 24737, upload-time = "2018-12-03T13:35:27.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/26/b935bbf9d27e898b87d80e7873a0200cebf239253d0afe7a59f82fe90fff/zope.deprecation-4.4.0-py2.py3-none-any.whl", hash = "sha256:f1480b74995958b24ce37b0ef04d3663d2683e5d6debc96726eff18acf4ea113", size = 10017, upload-time = "2018-12-03T13:35:25.25Z" }, -] - [[package]] name = "zope-event" -version = "4.5.0" +version = "6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/00/94ed30bfec18edbabfcbd503fcf7482c5031b0fbbc9bc361f046cb79781c/zope.event-4.5.0.tar.gz", hash = "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330", size = 18723, upload-time = "2020-09-18T10:19:55.685Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/d8/9c8b0c6bb1db09725395618f68d3b8a08089fca0aed28437500caaf713ee/zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92", size = 18731, upload-time = "2025-09-12T07:10:13.551Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/85/b45408c64f3b888976f1d5b37eed8d746b8d5729a66a49ec846fda27d371/zope.event-4.5.0-py2.py3-none-any.whl", hash = "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42", size = 6775, upload-time = "2020-09-18T10:19:54.005Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b5/1abb5a8b443314c978617bf46d5d9ad648bdf21058074e817d7efbb257db/zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3", size = 6409, upload-time = "2025-09-12T07:10:12.316Z" }, ] -[[package]] -name = "zope-hookable" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/ce/4d1cd6d2a3d980989ceaf86abb6c8683f90f01a81861f3de6ec7ae317db7/zope.hookable-5.0.1.tar.gz", hash = "sha256:29d07681a78042cdd15b268ae9decffed9ace68a53eebeb61d65ae931d158841", size = 24401, upload-time = "2020-03-10T13:22:37.365Z" } - [[package]] name = "zope-interface" version = "8.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/88/3a/7fcf02178b8fad0a51e67e32765cd039ae505d054d744d76b8c2bbcba5ba/zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1", size = 253746, upload-time = "2025-09-25T05:55:51.285Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/e5/ffef169d17b92c6236b3b18b890c0ce73502f3cbd5b6532ff20d412d94a3/zope_interface-8.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fd7195081b8637eeed8d73e4d183b07199a1dc738fb28b3de6666b1b55662570", size = 207364, upload-time = "2025-09-25T05:58:50.262Z" }, - { url = "https://files.pythonhosted.org/packages/35/b6/87aca626c09af829d3a32011599d6e18864bc8daa0ad3a7e258f3d7f8bcf/zope_interface-8.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f7c4bc4021108847bce763673ce70d0716b08dfc2ba9889e7bad46ac2b3bb924", size = 207901, upload-time = "2025-09-25T05:58:51.74Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c1/eec33cc9f847ebeb0bc6234d7d45fe3fc0a6fe8fc5b5e6be0442bd2c684d/zope_interface-8.0.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:758803806b962f32c87b31bb18c298b022965ba34fe532163831cc39118c24ab", size = 249358, upload-time = "2025-09-25T05:58:16.979Z" }, - { url = "https://files.pythonhosted.org/packages/58/7d/1e3476a1ef0175559bd8492dc7bb921ad0df5b73861d764b1f824ad5484a/zope_interface-8.0.1-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f8e88f35f86bbe8243cad4b2972deef0fdfca0a0723455abbebdc83bbab96b69", size = 254475, upload-time = "2025-09-25T05:58:10.032Z" }, - { url = "https://files.pythonhosted.org/packages/bc/67/ba5ea98ff23f723c5cbe7db7409f2e43c9fe2df1ced67881443c01e64478/zope_interface-8.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7844765695937d9b0d83211220b72e2cf6ac81a08608ad2b58f2c094af498d83", size = 254913, upload-time = "2025-09-25T06:26:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a7/b1b8b6c13fba955c043cdee409953ee85f652b106493e2e931a84f95c1aa/zope_interface-8.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:64fa7b206dd9669f29d5c1241a768bebe8ab1e8a4b63ee16491f041e058c09d0", size = 211753, upload-time = "2025-09-25T05:59:00.561Z" }, -] - -[[package]] -name = "zope-proxy" -version = "4.3.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, - { name = "zope-interface" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/37/26899cb231ecfa04822a17a669eac6df7ef0c2a86e2b78001db0cd3edd46/zope.proxy-4.3.5.tar.gz", hash = "sha256:a66a0d94e5b081d5d695e66d6667e91e74d79e273eee95c1747717ba9cb70792", size = 45162, upload-time = "2020-03-16T16:05:11.03Z" } [[package]] name = "zope-schema" -version = "6.0.1" +version = "8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, { name = "zope-event" }, { name = "zope-interface" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/9e1d4cfc77bd6f6b911a9fe790ae07a24f4765d524bd4f1d509a9ff7b7e7/zope.schema-6.0.1.tar.gz", hash = "sha256:9b3fc3ac656099aa9ebf3beb2bbd83d2d6ee6f94b9ac6969d6e3993ec9c4a197", size = 108325, upload-time = "2021-01-25T15:48:43.743Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/8d/1733d2bfcc9a579374706fe8049e1df203b79c0034002ecf4f5d11d7d548/zope_schema-8.0.tar.gz", hash = "sha256:b7fd5f9c1cb1460b2e4dd5a8e5a9a7bcaa72eb892a8e70690f615c4f98c0abaa", size = 109823, upload-time = "2025-09-12T07:38:51.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/5b/74b5212e1479dcbe712f0dfb5e5cbaea165331e765ec4f83df50b31b7af4/zope.schema-6.0.1-py2.py3-none-any.whl", hash = "sha256:a15982521241c660bf287a7e86b06df7131db00e40cee7365a2d5eadf2d051a6", size = 85814, upload-time = "2021-01-25T15:48:41.524Z" }, + { url = "https://files.pythonhosted.org/packages/51/80/f02bba656aedc8699733482f7904b159b9110631d212fc7335b176326f17/zope_schema-8.0-py3-none-any.whl", hash = "sha256:37c43a8d06a9afa6740a8df481f5b7690de8ea8a8c4af491d3bb697ab079eb6d", size = 84982, upload-time = "2025-09-12T07:38:49.912Z" }, ] diff --git a/versioning/templates/activity_feed_data.html b/versioning/templates/activity_feed_data.html index 7f7130af..6306ea35 100644 --- a/versioning/templates/activity_feed_data.html +++ b/versioning/templates/activity_feed_data.html @@ -1,4 +1,4 @@ -{% extends request.is_ajax|yesno:"base_ajax_nomodal.html,base_rigs.html" %} +{% extends is_ajax|yesno:"base_ajax_nomodal.html,base_rigs.html" %} {% load static %} {% load humanize %} diff --git a/versioning/tests/test_models.py b/versioning/tests/test_models.py index 51bb5b9e..e40ed1bb 100644 --- a/versioning/tests/test_models.py +++ b/versioning/tests/test_models.py @@ -9,27 +9,28 @@ from versioning import versioning class RIGSVersionTestCase(TestCase): def setUp(self): - models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at='2013-01-01') + models.VatRate.objects.create(rate=0.20, comment="TP V1", start_at="2013-01-01") self.profile = models.Profile.objects.get_or_create( - first_name='Test', - last_name='TEC User', - username='eventauthtest', - email='teccie@functional.test', - is_superuser=True # lazily grant all permissions + first_name="Test", + last_name="TEC User", + username="eventauthtest", + email="teccie@functional.test", + is_superuser=True, # lazily grant all permissions )[0] with reversion.create_revision(): reversion.set_user(self.profile) - self.person = models.Person.objects.create(name='Authorisation Test Person') + self.person = models.Person.objects.create(name="Authorisation Test Person") with reversion.create_revision(): reversion.set_user(self.profile) - self.organisation = models.Organisation.objects.create(name='Authorisation Test Organisation') + self.organisation = models.Organisation.objects.create(name="Authorisation Test Organisation") with reversion.create_revision(): reversion.set_user(self.profile) - self.event = models.Event.objects.create(name="AuthorisationTestCase", person=self.person, - start_date=date.today()) + self.event = models.Event.objects.create( + name="AuthorisationTestCase", person=self.person, start_date=date.today() + ) with reversion.create_revision(): reversion.set_user(self.profile) @@ -38,26 +39,27 @@ class RIGSVersionTestCase(TestCase): def test_find_parent_version(self): # Find the most recent version - current_version = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created') + current_version = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision_id") self.assertEqual(current_version._object_version.object.notes, "A new note on the event") # Check the prev version is loaded correctly previousVersion = current_version.parent - assert previousVersion._object_version.object.notes == '' + assert previousVersion._object_version.object.notes == "" # Check that finding the parent of the first version fails gracefully self.assertFalse(previousVersion.parent) def test_changes_since(self): # Find the most recent version - currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created') + currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision_id") changes = currentVersion.changes self.assertEqual(len(changes.field_changes), 1) def test_manager(self): objs = versioning.RIGSVersion.objects.get_for_multiple_models( - [models.Event, models.Person, models.Organisation]) + [models.Event, models.Person, models.Organisation] + ) self.assertEqual(len(objs), 4) def test_text_field_types(self): @@ -68,7 +70,7 @@ class RIGSVersionTestCase(TestCase): self.event.save() # Find the most recent version - currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created') + currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision_id") diff = currentVersion.changes # There are two changes @@ -90,7 +92,7 @@ class RIGSVersionTestCase(TestCase): self.person.save() # Find the most recent version - currentVersion = versioning.RIGSVersion.objects.get_for_object(self.person).latest('revision__date_created') + currentVersion = versioning.RIGSVersion.objects.get_for_object(self.person).latest("revision_id") diff = currentVersion.changes # Should be declared as long @@ -103,15 +105,18 @@ class RIGSVersionTestCase(TestCase): self.event.save() # Find the most recent version - currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created') + currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision_id") # Check the diff is correct - self.assertEqual(currentVersion.changes.field_changes[0].diff, - [{'type': 'equal', 'text': "A"}, - {'type': 'delete', 'text': " new"}, - {'type': 'insert', 'text': "n old"}, - {'type': 'equal', 'text': " note on the event"} - ]) + self.assertEqual( + currentVersion.changes.field_changes[0].diff, + [ + {"type": "equal", "text": "A"}, + {"type": "delete", "text": " new"}, + {"type": "insert", "text": "n old"}, + {"type": "equal", "text": " note on the event"}, + ], + ) def test_choice_field(self): with reversion.create_revision(): @@ -119,12 +124,12 @@ class RIGSVersionTestCase(TestCase): self.event.status = models.Event.CONFIRMED self.event.save() - currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created') - self.assertEqual(currentVersion.changes.field_changes[0].old, 'Provisional') - self.assertEqual(currentVersion.changes.field_changes[0].new, 'Confirmed') + currentVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision_id") + self.assertEqual(currentVersion.changes.field_changes[0].old, "Provisional") + self.assertEqual(currentVersion.changes.field_changes[0].new, "Confirmed") def test_creation_behaviour(self): - firstVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created').parent + firstVersion = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision__date_created").parent diff = firstVersion.changes # Mainly to check for exceptions: @@ -137,7 +142,7 @@ class RIGSVersionTestCase(TestCase): self.event.save() # Find the most recent version - current_version = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created') + current_version = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision_id") diffs = current_version.changes.item_changes @@ -156,7 +161,7 @@ class RIGSVersionTestCase(TestCase): item1.save() self.event.save() - current_version = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created') + current_version = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision_id") diffs = current_version.changes.item_changes @@ -166,10 +171,13 @@ class RIGSVersionTestCase(TestCase): self.assertEqual(diffs[0].new.name, "New Name") # Check the diff - self.assertEqual(current_version.changes.item_changes[0].field_changes[0].diff, - [{'type': 'delete', 'text': "TI I1"}, - {'type': 'insert', 'text': "New Name"}, - ]) + self.assertEqual( + current_version.changes.item_changes[0].field_changes[0].diff, + [ + {"type": "delete", "text": "TI I1"}, + {"type": "insert", "text": "New Name"}, + ], + ) # Delete the item @@ -178,7 +186,7 @@ class RIGSVersionTestCase(TestCase): self.event.save() # Find the most recent version - current_version = versioning.RIGSVersion.objects.get_for_object(self.event).latest('revision__date_created') + current_version = versioning.RIGSVersion.objects.get_for_object(self.event).latest("revision_id") diffs = current_version.changes.item_changes diff --git a/versioning/tests/test_unit.py b/versioning/tests/test_unit.py index 6abdcf38..fbb7b613 100644 --- a/versioning/tests/test_unit.py +++ b/versioning/tests/test_unit.py @@ -9,7 +9,7 @@ from assets import models as amodels def create_events(admin_user): - models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + models.VatRate.objects.create(start_at="2014-03-05", rate=0.20, comment="test1") events = {} @@ -19,7 +19,7 @@ def create_events(admin_user): with reversion.create_revision(): reversion.set_user(admin_user) - events[2] = models.Event.objects.create(name="TE E2", start_date='2014-03-05') + events[2] = models.Event.objects.create(name="TE E2", start_date="2014-03-05") with reversion.create_revision(): reversion.set_user(admin_user) @@ -38,13 +38,15 @@ def create_assets(admin_user): with reversion.create_revision(): reversion.set_user(admin_user) - assets[1] = amodels.Asset.objects.create(asset_id="1991", description="Spaceflower", status=broken, - category=lighting, date_acquired=date.today()) + assets[1] = amodels.Asset.objects.create( + asset_id="1991", description="Spaceflower", status=broken, category=lighting, date_acquired=date.today() + ) with reversion.create_revision(): reversion.set_user(admin_user) - assets[2] = amodels.Asset.objects.create(asset_id="0001", description="Virgil", status=working, - category=lighting, date_acquired=date.today()) + assets[2] = amodels.Asset.objects.create( + asset_id="0001", description="Virgil", status=working, category=lighting, date_acquired=date.today() + ) with reversion.create_revision(): reversion.set_user(admin_user) @@ -56,27 +58,27 @@ def create_assets(admin_user): def test_history_loads_successfully(admin_client, admin_user): events = create_events(admin_user) - request_url = reverse('event_history', kwargs={'pk': events[1].pk}) + request_url = reverse("event_history", kwargs={"pk": events[1].pk}) response = admin_client.get(request_url, follow=True) assert response.status_code == 200 assets = create_assets(admin_user) - request_url = reverse('asset_history', kwargs={'pk': assets[1].asset_id}) + request_url = reverse("asset_history", kwargs={"pk": assets[1].asset_id}) response = admin_client.get(request_url, follow=True) assert response.status_code == 200 def test_activity_feed_loads_successfully(admin_client): - request_url = reverse('activity_feed') + request_url = reverse("activity_feed") response = admin_client.get(request_url, follow=True) assert response.status_code == 200 def test_activity_table_loads_successfully(admin_client): - request_url = reverse('activity_table') + request_url = reverse("activity_table") response = admin_client.get(request_url, follow=True) assert response.status_code == 200 - request_url = reverse('assets_activity_table') + request_url = reverse("assets_activity_table") response = admin_client.get(request_url, follow=True) assert response.status_code == 200 @@ -84,7 +86,7 @@ def test_activity_table_loads_successfully(admin_client): # Some edge cases that have caused server errors in the past def test_deleted_event(admin_client, admin_user): events = create_events(admin_user) - request_url = reverse('activity_feed') + request_url = reverse("activity_feed") events[2].delete() @@ -95,7 +97,7 @@ def test_deleted_event(admin_client, admin_user): def test_deleted_relation(admin_client, admin_user): events = create_events(admin_user) - request_url = reverse('activity_feed') + request_url = reverse("activity_feed") with reversion.create_revision(): person = models.Person.objects.create(name="Test Person") diff --git a/versioning/urls.py b/versioning/urls.py index 34f46fa1..b460e361 100644 --- a/versioning/urls.py +++ b/versioning/urls.py @@ -1,44 +1,58 @@ from django.urls import path from django.apps import apps -from django.urls import path -from django.template.loader import get_template -from django.template import TemplateDoesNotExist -from PyRIGS.decorators import (permission_required_with_403) +from PyRIGS.decorators import permission_required_with_403 from versioning import views urlpatterns = [ - path('rigboard/activity/feed/', - permission_required_with_403('RIGS.view_event')(views.ActivityFeed.as_view()), - name='activity_feed'), + path( + "rigboard/activity/feed/", + permission_required_with_403("RIGS.view_event")(views.ActivityFeed.as_view()), + name="activity_feed", + ), ] for app in [apps.get_app_config(label) for label in ("RIGS", "assets", "training")]: appname = str(app.label) - if appname == 'RIGS': - appname = 'rigboard' - table_name = 'activity_table' + if appname == "RIGS": + appname = "rigboard" + table_name = "activity_table" else: - table_name = appname + '_activity_table' + table_name = appname + "_activity_table" # TODO Proper Permissions urlpatterns += [ - path(appname + '/activity/', permission_required_with_403('RIGS.add_event')(views.ActivityTable.as_view()), - name=table_name, kwargs={'app': appname, 'models': views.get_models(app.label)}), ] + path( + appname + "/activity/", + permission_required_with_403("RIGS.add_event")(views.ActivityTable.as_view()), + name=table_name, + kwargs={"app": appname, "models": views.get_models(app.label)}, + ), + ] for model in views.get_models(app=app.label): modelname = model.__name__.lower() - if appname == 'rigboard': + if appname == "rigboard": urlpatterns += [ - path(f'{modelname}//history/', - permission_required_with_403(f'{app.label}.change_{modelname}')( - views.VersionHistory.as_view()), - name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }), + path( + f"{modelname}//history/", + permission_required_with_403(f"{app.label}.change_{modelname}")(views.VersionHistory.as_view()), + name=f"{modelname}_history", + kwargs={ + "model": model, + "app": appname, + }, + ), ] else: urlpatterns += [ - path(f'{appname}/{modelname}//history/', - permission_required_with_403(f'{app.label}.change_{modelname}')( - views.VersionHistory.as_view()), - name=f'{modelname}_history', kwargs={'model': model, 'app': appname, }), + path( + f"{appname}/{modelname}//history/", + permission_required_with_403(f"{app.label}.change_{modelname}")(views.VersionHistory.as_view()), + name=f"{modelname}_history", + kwargs={ + "model": model, + "app": appname, + }, + ), ] diff --git a/versioning/versioning.py b/versioning/versioning.py index b546a7c5..89de43c6 100644 --- a/versioning/versioning.py +++ b/versioning/versioning.py @@ -1,4 +1,3 @@ -import logging from diff_match_patch import diff_match_patch from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist @@ -15,7 +14,7 @@ class RevisionMixin: @property def current_version(self): - version = RIGSVersion.objects.get_for_object(self).select_related('revision').first() + version = RIGSVersion.objects.get_for_object(self).select_related("revision").first() return version @property @@ -51,7 +50,11 @@ class FieldComparison: self._new = new def display_value(self, value): - if isinstance(self.field, (IntegerField, CharField)) and self.field.choices is not None and len(self.field.choices) > 0: + if ( + isinstance(self.field, (IntegerField, CharField)) + and self.field.choices is not None + and len(self.field.choices) > 0 + ): choice = [x[1] for x in self.field.choices if x[0] == value] if len(choice) > 0: return choice[0] @@ -92,18 +95,18 @@ class FieldComparison: outputDiffs = [] - for (op, data) in diffs: + for op, data in diffs: if op == dmp.DIFF_INSERT: - outputDiffs.append({'type': 'insert', 'text': data}) + outputDiffs.append({"type": "insert", "text": data}) elif op == dmp.DIFF_DELETE: - outputDiffs.append({'type': 'delete', 'text': data}) + outputDiffs.append({"type": "delete", "text": data}) elif op == dmp.DIFF_EQUAL: - outputDiffs.append({'type': 'equal', 'text': data}) + outputDiffs.append({"type": "equal", "text": data}) return outputDiffs class ModelComparison: - def __init__(self, old=None, new=None, version=None, follow=False, excluded_keys=['date_joined']): + def __init__(self, old=None, new=None, version=None, follow=False, excluded_keys=["date_joined"]): # recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects try: self.fields = old._meta.get_fields() @@ -151,12 +154,20 @@ class ModelComparison: if self.follow and self.version.object is not None: from RIGS.models import EventAuthorisation from training.models import TrainingLevelQualification, TrainingItemQualification - item_type = ContentType.objects.get_for_model(self.version.object) - old_item_versions = self.version.parent.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) \ - .exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification)) - new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude(content_type=ContentType.objects.get_for_model(EventAuthorisation)) - comparisonParams = {'excluded_keys': ['id', 'event', 'order', 'checklist', 'level', '_order', 'date_joined']} + item_type = ContentType.objects.get_for_model(self.version.object) + old_item_versions = ( + self.version.parent.revision.version_set.exclude(content_type=item_type) + .exclude(content_type=ContentType.objects.get_for_model(TrainingItemQualification)) + .exclude(content_type=ContentType.objects.get_for_model(TrainingLevelQualification)) + ) + new_item_versions = self.version.revision.version_set.exclude(content_type=item_type).exclude( + content_type=ContentType.objects.get_for_model(EventAuthorisation) + ) + + comparisonParams = { + "excluded_keys": ["id", "event", "order", "checklist", "level", "_order", "date_joined"] + } # Build some dicts of what we have item_dict = {} # build a list of items, key is the item_pk @@ -178,7 +189,7 @@ class ModelComparison: item_dict[version.object_id] = compare # update the dictionary with the changes changes = [] - for (_, compare) in list(item_dict.items()): + for _, compare in list(item_dict.items()): if compare.fields_changed: changes.append(compare) @@ -188,7 +199,7 @@ class ModelComparison: def name(self): obj = self.new if self.new else self.old - if (hasattr(obj, 'activity_feed_string')): + if hasattr(obj, "activity_feed_string"): return obj.activity_feed_string else: return str(obj) @@ -208,8 +219,13 @@ class RIGSVersionManager(VersionQuerySet): for model in model_array: content_types.append(ContentType.objects.get_for_model(model)) - return self.filter(content_type__in=content_types).select_related("revision",).order_by( - "-revision__date_created") + return ( + self.filter(content_type__in=content_types) + .select_related( + "revision", + ) + .order_by("-revision__date_created") + ) class RIGSVersion(Version): @@ -223,11 +239,14 @@ class RIGSVersion(Version): def parent(self): thisId = self.object_id - versions = RIGSVersion.objects.get_for_object_reference(self.content_type.model_class(), thisId).select_related( - "revision", "revision__user").all() + versions = ( + RIGSVersion.objects.get_for_object_reference(self.content_type.model_class(), thisId) + .select_related("revision", "revision__user") + .all() + ) try: - previousVersion = versions.filter(revision_id__lt=self.revision_id).latest('revision__date_created') + previousVersion = versions.filter(revision_id__lt=self.revision_id).latest("revision_id") except ObjectDoesNotExist: return False @@ -239,7 +258,7 @@ class RIGSVersion(Version): version=self, new=self._object_version.object, old=self.parent._object_version.object if self.parent else None, - follow=True + follow=True, ) @property @@ -248,7 +267,7 @@ class RIGSVersion(Version): @property def display_name(self): - if hasattr(self.changes.new, 'display_id'): + if hasattr(self.changes.new, "display_id"): id = self.changes.new.display_id else: id = self.changes.new.pk diff --git a/versioning/views.py b/versioning/views.py index 354d7ccd..6a91a9a0 100644 --- a/versioning/views.py +++ b/versioning/views.py @@ -15,28 +15,31 @@ class VersionHistory(generic.ListView): paginate_by = 25 def get_queryset(self, **kwargs): - return RIGSVersion.objects.get_for_object(self.get_object()).select_related("revision", - "revision__user").all().order_by( - "-revision__date_created") + return ( + RIGSVersion.objects.get_for_object(self.get_object()) + .select_related("revision", "revision__user") + .all() + .order_by("-revision__date_created") + ) def get_object(self, **kwargs): # Goddamit, almost got away without specific hacks - if self.kwargs['model'].__name__ == 'Asset': - return get_object_or_404(self.kwargs['model'], asset_id=self.kwargs['pk']) + if self.kwargs["model"].__name__ == "Asset": + return get_object_or_404(self.kwargs["model"], asset_id=self.kwargs["pk"]) else: - return get_object_or_404(self.kwargs['model'], pk=self.kwargs['pk']) + return get_object_or_404(self.kwargs["model"], pk=self.kwargs["pk"]) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['object'] = self.get_object() - if self.kwargs['app'] != 'rigboard': - context['override'] = f'base_{self.kwargs["app"]}.html' + context["object"] = self.get_object() + if self.kwargs["app"] != "rigboard": + context["override"] = f"base_{self.kwargs['app']}.html" return context def get_models(app=None): - models = filter(lambda item: not hasattr(item, 'reversion_hide'), reversion.get_registered_models()) + models = filter(lambda item: not hasattr(item, "reversion_hide"), reversion.get_registered_models()) if app is not None: models = filter(lambda item: item in apps.get_app_config(app).get_models(), models) # Don't allow modifying original list! @@ -46,7 +49,9 @@ def get_models(app=None): # TODO Default filter of having permission to view associated object def filter_models(models, user): if user is not None: - models = filter(lambda model: not hasattr(model, 'reversion_perm') or user.has_perm(model.reversion_perm), models) + models = filter( + lambda model: not hasattr(model, "reversion_perm") or user.has_perm(model.reversion_perm), models + ) return models @@ -56,13 +61,15 @@ class ActivityTable(generic.ListView): paginate_by = 25 def get_queryset(self): - return RIGSVersion.objects.get_for_multiple_models(filter_models(self.kwargs.get('models'), self.request.user)).order_by("-revision__date_created") + return RIGSVersion.objects.get_for_multiple_models( + filter_models(self.kwargs.get("models"), self.request.user) + ).order_by("-revision__date_created") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['page_title'] = f"{title(self.kwargs['app'])} Activity Stream" - if self.kwargs['app'] != 'rigboard': - context['override'] = f'base_{self.kwargs["app"]}.html' + context["page_title"] = f"{title(self.kwargs['app'])} Activity Stream" + if self.kwargs["app"] != "rigboard": + context["override"] = f"base_{self.kwargs['app']}.html" return context @@ -73,17 +80,19 @@ class ActivityFeed(generic.ListView): # Appears on homepage paginate_by = 25 def get_queryset(self): - return RIGSVersion.objects.get_for_multiple_models(filter_models(get_models(), self.request.user)).order_by("-revision__date_created") + return RIGSVersion.objects.get_for_multiple_models(filter_models(get_models(), self.request.user)).order_by( + "-revision__date_created" + ) def get_context_data(self, **kwargs): # Call the base implementation first to get a context context = super().get_context_data(**kwargs) - context['page_title'] = "Activity Feed" + context["page_title"] = "Activity Feed" maxTimeDelta = datetime.timedelta(hours=1) items = [] - for thisVersion in context['object_list']: + for thisVersion in context["object_list"]: thisVersion.withPrevious = False if len(items) >= 1: timeDiff = items[-1].revision.date_created - thisVersion.revision.date_created