From 2706cb3a244f40ce00843d66fb9bb178370e062e Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Mon, 11 Nov 2024 17:50:02 +0100 Subject: [PATCH] Switch to ESM and Baseline2024 (#311) * Drop custom upload progress events * Update browser support to Baseline 2024 * Update Django version support * Update Python version support * Fix #310 -- Add `formaction` support on submit buttons --- .github/workflows/ci.yml | 52 +- .gitignore | 1 + README.md | 49 +- package-lock.json | 781 ++++++++++++++++++++++++++++-- package.json | 12 +- pyproject.toml | 8 +- s3file/__init__.py | 5 - s3file/checks.py | 2 +- s3file/forms.py | 45 +- s3file/static/s3file/js/s3file.js | 234 ++++----- s3file/storages.py | 2 +- tests/__tests__/s3file.test.js | 158 ++++++ tests/test_apps.py | 6 +- tests/test_forms.py | 100 +++- tests/testapp/templates/form.html | 35 +- tests/testapp/views.py | 1 + 16 files changed, 1187 insertions(+), 304 deletions(-) create mode 100644 tests/__tests__/s3file.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96b9974..bf63ad3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,33 +11,45 @@ jobs: dist: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.x" - - uses: actions/checkout@v4 - run: python -m pip install --upgrade pip build wheel twine - run: python -m build --sdist --wheel - run: python -m twine check dist/* - standardjs: + js-lint: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '14.x' + node-version-file: .nvmrc + - name: Install Node dependencies + run: npm ci + - run: npm run lint:js + + + js-test: + runs-on: ubuntu-latest + needs: + - js-lint + steps: - uses: actions/checkout@v4 - - id: cache-npm - uses: actions/cache@v4 + - uses: actions/setup-node@v4 with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- + node-version-file: .nvmrc - name: Install Node dependencies run: npm ci - - run: npm run lint:js + - run: node --test --experimental-test-coverage --test-reporter=spec --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=lcov.txt + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: javascript + file: lcov.txt - lint: + py-lint: runs-on: ubuntu-latest strategy: matrix: @@ -59,20 +71,19 @@ jobs: pytest: needs: - - lint - - standardjs + - py-lint - dist runs-on: ubuntu-latest strategy: matrix: python-version: - - "3.10" - "3.11" - "3.12" + - "3.13" django-version: - - "3.2" - "4.2" - "5.0" + - "5.1" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -90,12 +101,16 @@ jobs: curl -qO "https://chromedriver.storage.googleapis.com/$(curl -q https://chromedriver.storage.googleapis.com/LATEST_RELEASE)/chromedriver_linux64.zip" unzip chromedriver_linux64.zip -d bin - - run: python -m pip install .[test] codecov + - run: python -m pip install .[test] - run: python -m pip install django~=${{ matrix.django-version }}.0 - run: python -m pytest -m "not selenium" env: PATH: $PATH:$(pwd)/bin - - run: codecov + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: python + selenium: needs: @@ -120,6 +135,9 @@ jobs: - run: python -m pip install -e .[test] - run: python -m pytest -m selenium - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: selenium analyze: diff --git a/.gitignore b/.gitignore index cfb6375..cf768ef 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +lcov.txt # Translations *.mo diff --git a/README.md b/README.md index 54a3443..90eda41 100644 --- a/README.md +++ b/README.md @@ -140,59 +140,12 @@ to your CORS policy. ] ``` -### Progress Bar - -S3File does emit progress signals that can be used to display some kind -of progress bar. Signals named `progress` are emitted for both each -individual file input as well as for the form as a whole. - -The progress signal carries the following details: - -```javascript -console.log(event.detail) - -{ - progress: 0.4725307607171312 // total upload progress of either a form or single input - loaded: 1048576 // total upload progress of either a form or single input - total: 2219064 // total bytes to upload - currentFile: File {…} // file object - currentFileName: "text.txt" // file name of the file currently uploaded - currentFileProgress: 0.47227834703299176 // upload progress of that file - originalEvent: ProgressEvent {…} // the original XHR onprogress event -} -``` - -The following example implements a Boostrap progress bar for upload -progress of an entire form. - -```html -
-
0%
-
-``` - -```javascript -(function () { - var form = document.getElementsByTagName('form')[0] - var progressBar = document.getElementsByClassName('progress-bar')[0] - - form.addEventListener('progress', function (event) { - // event.detail.progress is a value between 0 and 1 - var percent = Math.round(event.detail.progress * 100) - - progressBar.setAttribute('style', 'width:' + percent + '%') - progressBar.setAttribute('aria-valuenow', percent) - progressBar.innerText = percent + '%' - }) -})() -``` - ### Using S3File in development Using S3File in development can be helpful especially if you want to use the progress signals described above. Therefore, S3File comes with a AWS S3 dummy backend. It behaves similar to the real S3 storage backend. It -is automatically enabled, if the `DEFAULT_FILE_STORAGE` setting is set +is automatically enabled, if the `STORAGES["default"]` setting is set to `FileSystemStorage`. To prevent users from accidentally using the `FileSystemStorage` and the diff --git a/package-lock.json b/package-lock.json index 5a8f009..7b665a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,12 +5,13 @@ "requires": true, "packages": { "": { + "name": "django-s3file", "version": "1.0.0", - "hasInstallScript": true, "license": "MIT", "devDependencies": { - "standard": "*", - "uglify-js": "*" + "global-jsdom": "*", + "jsdom": "*", + "standard": "*" } }, "node_modules/@eslint-community/eslint-utils": { @@ -164,6 +165,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -340,6 +354,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -457,6 +478,19 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -477,6 +511,33 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -545,6 +606,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -585,6 +653,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -597,6 +675,19 @@ "node": ">=6.0.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1343,6 +1434,21 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1465,6 +1571,19 @@ "node": ">=10.13.0" } }, + "node_modules/global-jsdom": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/global-jsdom/-/global-jsdom-25.0.0.tgz", + "integrity": "sha512-Y8dUX6R5Aw5/cutvBY8ofSs2TJyHC3WVGAQGIhCeWlIpKjYcydh3APbxQaeKSfrawVO/YUQ0MAFJfjQDOPVY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "jsdom": ">=25 <26" + } + }, "node_modules/globals": { "version": "13.20.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", @@ -1612,6 +1731,60 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1894,6 +2067,13 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -2065,6 +2245,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -2191,6 +2412,29 @@ "node": ">=10" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2221,6 +2465,13 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2408,6 +2659,19 @@ "node": ">=4" } }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -2545,10 +2809,11 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2681,6 +2946,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -2739,6 +3011,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3027,12 +3319,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/tldts": { + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.56.tgz", + "integrity": "sha512-2PT1oRZCxtsbLi5R2SQjE/v4vvgRggAtVcYj+3Rrcnu2nPZvu7m64+gDa/EsVSWd3QzEc0U0xN+rbEKsJC47kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.56" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.56.tgz", + "integrity": "sha512-Ihxv/Bwiyj73icTYVgBUkQ3wstlCglLoegSgl64oSrGUBX1hc7Qmf/CnrnJLaQdZrCnTaLqMYOwKMKlkfkFrxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -3142,18 +3487,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -3187,6 +3520,66 @@ "node": ">=0.10.48" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3296,6 +3689,28 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", @@ -3305,6 +3720,23 @@ "node": ">=8" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -3431,6 +3863,15 @@ "dev": true, "requires": {} }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3555,6 +3996,12 @@ "is-shared-array-buffer": "^1.0.2" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3644,6 +4091,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3661,6 +4117,25 @@ "which": "^2.0.1" } }, + "cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "requires": { + "rrweb-cssom": "^0.7.1" + } + }, + "data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "requires": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + } + }, "data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3703,6 +4178,12 @@ "ms": "2.1.2" } }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3731,6 +4212,12 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3740,6 +4227,12 @@ "esutils": "^2.0.2" } }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -4294,6 +4787,17 @@ "is-callable": "^1.1.3" } }, + "form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4377,6 +4881,13 @@ "is-glob": "^4.0.3" } }, + "global-jsdom": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/global-jsdom/-/global-jsdom-25.0.0.tgz", + "integrity": "sha512-Y8dUX6R5Aw5/cutvBY8ofSs2TJyHC3WVGAQGIhCeWlIpKjYcydh3APbxQaeKSfrawVO/YUQ0MAFJfjQDOPVY8Q==", + "dev": true, + "requires": {} + }, "globals": { "version": "13.20.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", @@ -4476,6 +4987,44 @@ "function-bind": "^1.1.2" } }, + "html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^3.1.1" + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "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==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -4662,6 +5211,12 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4779,6 +5334,35 @@ "argparse": "^2.0.1" } }, + "jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "requires": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + } + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -4880,6 +5464,21 @@ "yallist": "^4.0.0" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4907,6 +5506,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5037,6 +5642,15 @@ "json-parse-better-errors": "^1.0.1" } }, + "parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "requires": { + "entities": "^4.5.0" + } + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -5140,9 +5754,9 @@ } }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "queue-microtask": { @@ -5222,6 +5836,12 @@ "glob": "^7.1.3" } }, + "rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5254,6 +5874,21 @@ "is-regex": "^1.1.4" } }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5442,12 +6077,51 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "tldts": { + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.56.tgz", + "integrity": "sha512-2PT1oRZCxtsbLi5R2SQjE/v4vvgRggAtVcYj+3Rrcnu2nPZvu7m64+gDa/EsVSWd3QzEc0U0xN+rbEKsJC47kA==", + "dev": true, + "requires": { + "tldts-core": "^6.1.56" + } + }, + "tldts-core": { + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.56.tgz", + "integrity": "sha512-Ihxv/Bwiyj73icTYVgBUkQ3wstlCglLoegSgl64oSrGUBX1hc7Qmf/CnrnJLaQdZrCnTaLqMYOwKMKlkfkFrxQ==", + "dev": true + }, + "tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "requires": { + "tldts": "^6.1.32" + } + }, + "tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "requires": { + "punycode": "^2.3.1" + } + }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -5527,12 +6201,6 @@ "possible-typed-array-names": "^1.0.0" } }, - "uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true - }, "unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -5560,6 +6228,46 @@ "integrity": "sha512-MGQLX89UxmYHgDvcXyjBI0cbmoW+t/dANDppNPrno64rYr8nH4SHSuElQuSYdXGEs0mUzdQe1BY+FhVPNsAmJQ==", "dev": true }, + "w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "requires": { + "xml-name-validator": "^5.0.0" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "requires": { + "iconv-lite": "0.6.3" + } + }, + "whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true + }, + "whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "requires": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5639,12 +6347,31 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "requires": {} + }, "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, + "xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 9de3349..a565644 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,7 @@ "test": "tests" }, "scripts": { - "test": "standard", - "postinstall": "uglifyjs --compress -o s3file/static/s3file/js/s3file.min.js s3file/static/s3file/js/s3file.js", - "minify": "uglifyjs --compress -o s3file/static/s3file/js/s3file.min.js s3file/static/s3file/js/s3file.js", + "test": "node --test --experimental-test-coverage", "lint:js": "standard" }, "repository": { @@ -21,14 +19,16 @@ "django", "file" ], - "author": "Johannes Hoppe ", + "author": "Johannes Maron ", "license": "MIT", + "type": "module", "bugs": { "url": "https://github.com/codingjoe/django-s3file/issues" }, "homepage": "https://github.com/codingjoe/django-s3file#readme", "devDependencies": { - "standard": "*", - "uglify-js": "*" + "global-jsdom": "*", + "jsdom": "*", + "standard": "*" } } diff --git a/pyproject.toml b/pyproject.toml index 42e381a..edde257 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,17 +21,17 @@ classifiers = [ "Topic :: Software Development", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Framework :: Django", - "Framework :: Django :: 3.2", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", ] -requires-python = ">=3.9" +requires-python = ">=3.11" dependencies = [ - "django>=2.0", + "django>=4.2", "django-storages>=1.6", "boto3", ] diff --git a/s3file/__init__.py b/s3file/__init__.py index 94bb8d5..5435812 100644 --- a/s3file/__init__.py +++ b/s3file/__init__.py @@ -1,11 +1,6 @@ """A lightweight file uploader input for Django and Amazon S3.""" -import django - from . import _version __version__ = _version.version VERSION = _version.version_tuple - -if django.VERSION < (4, 0): - default_app_config = "s3file.apps.S3FileConfig" diff --git a/s3file/checks.py b/s3file/checks.py index 5c60159..8f7911c 100644 --- a/s3file/checks.py +++ b/s3file/checks.py @@ -7,7 +7,7 @@ def storage_check(app_configs, **kwargs): return [ Error( "FileSystemStorage should not be used in a production environment.", - hint="Please verify your DEFAULT_FILE_STORAGE setting.", + hint='Please verify your STORAGES["default"] setting.', id="s3file.E001", ) ] diff --git a/s3file/forms.py b/s3file/forms.py index 78bfde7..a5006bb 100644 --- a/s3file/forms.py +++ b/s3file/forms.py @@ -4,7 +4,9 @@ import uuid from django.conf import settings +from django.templatetags.static import static from django.utils.functional import cached_property +from django.utils.html import format_html, html_safe from storages.utils import safe_join from s3file.middleware import S3FileMiddleware @@ -13,6 +15,42 @@ logger = logging.getLogger("s3file") +@html_safe +class Asset: + """A generic asset that can be included in a template.""" + + def __init__(self, path): + self.path = path + + def __eq__(self, other): + return (self.__class__ is other.__class__ and self.path == other.path) or ( + other.__class__ is str and self.path == other + ) + + def __hash__(self): + return hash(self.path) + + def __str__(self): + return self.absolute_path(self.path) + + def absolute_path(self, path): + if path.startswith(("http://", "https://", "/")): + return path + return static(path) + + def __repr__(self): + return f"{type(self).__qualname__}: {self.path!r}" + + +class ESM(Asset): + """A JavaScript asset for ECMA Script Modules (ESM).""" + + def __str__(self): + path = super().__str__() + template = '' + return format_html(template, self.absolute_path(path)) + + class S3FileInputMixin: """FileInput that uses JavaScript to directly upload to Amazon S3.""" @@ -37,6 +75,7 @@ def client(self): def build_attrs(self, *args, **kwargs): attrs = super().build_attrs(*args, **kwargs) + attrs["is"] = "s3-file" accept = attrs.get("accept") response = self.client.generate_presigned_post( @@ -56,10 +95,6 @@ def build_attrs(self, *args, **kwargs): ) defaults.update(attrs) - try: - defaults["class"] += " s3file" - except KeyError: - defaults["class"] = "s3file" return defaults def get_conditions(self, accept): @@ -91,4 +126,4 @@ def upload_folder(self): ) # S3 uses POSIX paths class Media: - js = ("s3file/js/s3file.js" if settings.DEBUG else "s3file/js/s3file.min.js",) + js = [ESM("s3file/js/s3file.js")] diff --git a/s3file/static/s3file/js/s3file.js b/s3file/static/s3file/js/s3file.js index ad88901..43d62cc 100644 --- a/s3file/static/s3file/js/s3file.js +++ b/s3file/static/s3file/js/s3file.js @@ -1,158 +1,118 @@ -'use strict'; +/** + * Parse XML response from AWS S3 and return the file key. + * + * @param {string} responseText - XML response form AWS S3. + * @return {string} - Key from response. + */ +export function getKeyFromResponse (responseText) { + const xml = new globalThis.DOMParser().parseFromString(responseText, 'text/xml') + return decodeURI(xml.querySelector('Key').innerHTML) +} -(function () { - function parseURL (text) { - var xml = new window.DOMParser().parseFromString(text, 'text/xml') - var tag = xml.getElementsByTagName('Key')[0] - return decodeURI(tag.childNodes[0].nodeValue) +/** + * Custom element to upload files to AWS S3. + * + * @extends HTMLInputElement + */ +export class S3FileInput extends globalThis.HTMLInputElement { + constructor () { + super() + this.type = 'file' + this.keys = [] + this.upload = null } - function waitForAllFiles (form) { - if (window.uploading !== 0) { - setTimeout(function () { - waitForAllFiles(form) - }, 100) - } else { - window.HTMLFormElement.prototype.submit.call(form) - } + connectedCallback () { + this.form.addEventListener('formdata', this.fromDataHandler.bind(this)) + this.form.addEventListener('submit', this.submitHandler.bind(this), { once: true }) + this.form.addEventListener('upload', this.uploadHandler.bind(this)) + this.addEventListener('change', this.changeHandler.bind(this)) } - function request (method, url, data, fileInput, file, form) { - file.loaded = 0 - return new Promise(function (resolve, reject) { - var xhr = new window.XMLHttpRequest() + changeHandler () { + this.keys = [] + this.upload = null + try { + this.form.removeEventListener('submit', this.submitHandler.bind(this)) + } catch (error) { + console.debug(error) + } + this.form.addEventListener('submit', this.submitHandler.bind(this), { once: true }) + } - xhr.onload = function () { - if (xhr.status === 201) { - resolve(xhr.responseText) - } else { - reject(xhr.statusText) - } - } + /** + * Submit the form after uploading the files to S3. + * + * @param {SubmitEvent} event - The submit event. + * @return {Promise} + */ + async submitHandler (event) { + event.preventDefault() + this.form.dispatchEvent(new window.CustomEvent('upload')) + await Promise.all(this.form.pendingRquests) + this.form.requestSubmit(event.submitter) + } - xhr.upload.onprogress = function (e) { - var diff = e.loaded - file.loaded - form.loaded += diff - fileInput.loaded += diff - file.loaded = e.loaded - var defaultEventData = { - currentFile: file, - currentFileName: file.name, - currentFileProgress: Math.min(e.loaded / e.total, 1), - originalEvent: e - } - form.dispatchEvent(new window.CustomEvent('progress', { - detail: Object.assign({ - progress: Math.min(form.loaded / form.total, 1), - loaded: form.loaded, - total: form.total - }, defaultEventData) - })) - fileInput.dispatchEvent(new window.CustomEvent('progress', { - detail: Object.assign({ - progress: Math.min(fileInput.loaded / fileInput.total, 1), - loaded: fileInput.loaded, - total: fileInput.total - }, defaultEventData) - })) - } + uploadHandler () { + if (this.files.length && !this.upload) { + this.upload = this.uploadFiles() + this.form.pendingRquests = this.form.pendingRquests || [] + this.form.pendingRquests.push(this.upload) + } + } - xhr.onerror = function () { - reject(xhr.statusText) + /** + * Append the file key to the form data. + * + * @param {FormDataEvent} event - The formdata event. + */ + fromDataHandler (event) { + if (this.keys.length) { + event.formData.delete(this.name) + for (const key of this.keys) { + event.formData.append(this.name, key) } - - xhr.open(method, url) - xhr.send(data) - }) + event.formData.append('s3file', this.name) + event.formData.set(`${this.name}-s3f-signature`, this.dataset.s3fSignature) + } } - function uploadFiles (form, fileInput, name) { - var url = fileInput.getAttribute('data-url') - fileInput.loaded = 0 - fileInput.total = 0 - var promises = Array.from(fileInput.files).map(function (file) { - form.total += file.size - fileInput.total += file.size - var s3Form = new window.FormData() - Array.from(fileInput.attributes).forEach(function (attr) { - var name = attr.name + /** + * Upload files to AWS S3 and populate the keys array. + * + * @return {Promise} + */ + async uploadFiles () { + this.keys = [] + for (const file of this.files) { + const s3Form = new globalThis.FormData() + for (const attr of this.attributes) { + let name = attr.name if (name.startsWith('data-fields')) { name = name.replace('data-fields-', '') s3Form.append(name, attr.value) } - }) + } s3Form.append('success_action_status', '201') s3Form.append('Content-Type', file.type) s3Form.append('file', file) - return request('POST', url, s3Form, fileInput, file, form) - }) - Promise.all(promises).then(function (results) { - results.forEach(function (result) { - var hiddenFileInput = document.createElement('input') - hiddenFileInput.type = 'hidden' - hiddenFileInput.name = name - hiddenFileInput.value = parseURL(result) - form.appendChild(hiddenFileInput) - }) - fileInput.name = '' - window.uploading -= 1 - }, function (err) { - console.log(err) - fileInput.setCustomValidity(err) - fileInput.reportValidity() - }) - } - - function clickSubmit (e) { - var submitButton = e.currentTarget - var form = submitButton.closest('form') - var submitInput = document.createElement('input') - submitInput.type = 'hidden' - submitInput.value = submitButton.value || '1' - submitInput.name = submitButton.name - form.appendChild(submitInput) - } - - function uploadS3Inputs (form) { - window.uploading = 0 - form.loaded = 0 - form.total = 0 - var inputs = Array.from(form.querySelectorAll('input[type=file].s3file')) - - inputs.forEach(function (input) { - var hiddenS3Input = document.createElement('input') - hiddenS3Input.type = 'hidden' - hiddenS3Input.name = 's3file' - hiddenS3Input.value = input.name - form.appendChild(hiddenS3Input) - var hiddenSignatureInput = document.createElement('input') - hiddenSignatureInput.type = 'hidden' - hiddenSignatureInput.name = input.name + '-s3f-signature' - hiddenSignatureInput.value = input.dataset.s3fSignature - form.appendChild(hiddenSignatureInput) - }) - inputs.forEach(function (input) { - window.uploading += 1 - uploadFiles(form, input, input.name) - }) - waitForAllFiles(form) + console.debug('uploading', this.dataset.url, file) + try { + const response = await fetch(this.dataset.url, { method: 'POST', body: s3Form }) + if (response.status === 201) { + this.keys.push(getKeyFromResponse(await response.text())) + } else { + this.setCustomValidity(response.statusText) + this.reportValidity() + } + } catch (error) { + console.error(error) + this.setCustomValidity(error) + this.reportValidity() + } + } } +} - document.addEventListener('DOMContentLoaded', function () { - var forms = Array.from(document.querySelectorAll('input[type=file].s3file')).map(function (input) { - return input.closest('form') - }) - forms = new Set(forms) - forms.forEach(function (form) { - form.addEventListener('submit', function (e) { - e.preventDefault() - uploadS3Inputs(e.target) - }) - var submitButtons = form.querySelectorAll('input[type=submit], button[type=submit]') - Array.from(submitButtons).forEach(function (submitButton) { - submitButton.addEventListener('click', clickSubmit) - }) - }) - }) -})() +globalThis.customElements.define('s3-file', S3FileInput, { extends: 'input' }) diff --git a/s3file/storages.py b/s3file/storages.py index a29cd68..351fd25 100644 --- a/s3file/storages.py +++ b/s3file/storages.py @@ -21,7 +21,7 @@ class client: def generate_presigned_post(bucket_name, key, **policy): policy = json.dumps(policy).encode() policy_b64 = base64.b64encode(policy).decode() - date = datetime.datetime.now(tz=datetime.timezone.utc).strftime( + date = datetime.datetime.now(tz=datetime.UTC).strftime( "%Y%m%dT%H%M%SZ" ) aws_id = getattr( diff --git a/tests/__tests__/s3file.test.js b/tests/__tests__/s3file.test.js new file mode 100644 index 0000000..d46564c --- /dev/null +++ b/tests/__tests__/s3file.test.js @@ -0,0 +1,158 @@ +import 'global-jsdom/register' +import assert from 'node:assert' +import { afterEach, describe, mock, test } from 'node:test' +import * as s3file from '../../s3file/static/s3file/js/s3file.js' + +afterEach(() => { + mock.restoreAll() +}) + +describe('getKeyFromResponse', () => { + test('returns key', () => { + const responseText = + ` + + https://example-bucket.s3.amazonaws.com/tmp%2Fs2file%2Fsome-file.jpeg + example-bucket + tmp/s2file/some%20file.jpeg + "a38155039ec348f97dfd63da4cb2619d" + ` + assert.strictEqual(s3file.getKeyFromResponse(responseText), 'tmp/s2file/some file.jpeg') + }) +}) + +describe('S3FileInput', () => { + test('constructor', () => { + const input = new s3file.S3FileInput() + assert.strictEqual(input.type, 'file') + assert.deepStrictEqual(input.keys, []) + assert.strictEqual(input.upload, null) + }) + + test('connectedCallback', () => { + const form = document.createElement('form') + document.body.appendChild(form) + const input = new s3file.S3FileInput() + input.addEventListener = mock.fn(input.addEventListener) + form.addEventListener = mock.fn(form.addEventListener) + form.appendChild(input) + assert(form.addEventListener.mock.calls.length === 3) + assert(input.addEventListener.mock.calls.length === 1) + }) + + test('changeHandler', () => { + const form = document.createElement('form') + const input = new s3file.S3FileInput() + input.keys = ['key'] + input.upload = 'upload' + form.appendChild(input) + input.changeHandler() + assert(!input.keys.length) + assert(!input.upload) + }) + + test('submitHandler', async () => { + const form = document.createElement('form') + document.body.appendChild(form) + form.pendingRquests = [] + form.requestSubmit = mock.fn(form.requestSubmit) + form.dispatchEvent = mock.fn(form.dispatchEvent) + const submitButton = document.createElement('button') + form.appendChild(submitButton) + submitButton.setAttribute('type', 'submit') + const event = new window.SubmitEvent('submit', { submitter: submitButton }) + const input = new s3file.S3FileInput() + form.appendChild(input) + await input.submitHandler(event) + assert(form.dispatchEvent.mock.calls.length === 2) + assert(form.requestSubmit.mock.calls.length === 2) + }) + + test('uploadHandler', () => { + const form = document.createElement('form') + document.body.appendChild(form) + const input = new s3file.S3FileInput() + form.appendChild(input) + Object.defineProperty(input, 'files', { + get: () => [new globalThis.File([''], 'file.txt')] + }) + assert(!input.upload) + assert.strictEqual(input.files.length, 1) + input.uploadHandler() + console.log(input.upload) + assert(input.upload) + assert(form.pendingRquests) + }) + + test('fromDataHandler', () => { + const event = new globalThis.CustomEvent('formdata', { formData: new FormData() }) + const form = document.createElement('form') + document.body.appendChild(form) + const input = new s3file.S3FileInput() + form.appendChild(input) + input.name = 'file' + input.keys = ['key1', 'key2'] + event.formData = new FormData() + input.fromDataHandler(event) + assert.deepStrictEqual(event.formData.getAll('file'), ['key1', 'key2']) + assert.strictEqual(event.formData.get('s3file'), 'file') + }) + + test('uploadFiles', async () => { + const form = document.createElement('form') + document.body.appendChild(form) + const input = new s3file.S3FileInput() + input.setAttribute('data-fields-policy', 'policy') + form.appendChild(input) + Object.defineProperty(input, 'files', { + get: () => [new globalThis.File([''], 'file.txt')] + }) + const responseText = + ` + + https://example-bucket.s3.amazonaws.com/tmp%2Fs2file%2Fsome-file.jpeg + example-bucket + tmp/s2file/some%20file.jpeg + "a38155039ec348f97dfd63da4cb2619d" + ` + const response = { status: 201, text: async () => responseText } + globalThis.fetch = mock.fn(async () => response) + assert(input.files.length === 1) + await input.uploadFiles() + assert(globalThis.fetch.mock.calls.length === 1) + assert.deepStrictEqual(input.keys, ['tmp/s2file/some file.jpeg']) + }) + + test('uploadFiles with HTTP error', async () => { + const form = document.createElement('form') + document.body.appendChild(form) + const input = new s3file.S3FileInput() + form.appendChild(input) + Object.defineProperty(input, 'files', { + get: () => [new globalThis.File([''], 'file.txt')] + }) + const response = { status: 400, statusText: 'Bad Request' } + globalThis.fetch = mock.fn(async () => response) + assert(input.files.length === 1) + await input.uploadFiles() + assert(globalThis.fetch.mock.calls.length === 1) + assert.deepStrictEqual(input.keys, []) + assert.strictEqual(input.validationMessage, 'Bad Request') + }) + + test('uploadFiles with network error', async () => { + const form = document.createElement('form') + document.body.appendChild(form) + const input = new s3file.S3FileInput() + form.appendChild(input) + Object.defineProperty(input, 'files', { + get: () => [new globalThis.File([''], 'file.txt')] + }) + globalThis.fetch = mock.fn(async () => { throw new Error('Network Error') }) + assert(input.files.length === 1) + await input.uploadFiles() + assert(globalThis.fetch.mock.calls.length === 1) + assert.deepStrictEqual(input.keys, []) + assert.strictEqual(input.validationMessage, 'Error: Network Error') + }) +}) diff --git a/tests/test_apps.py b/tests/test_apps.py index 2215d93..0dc61d9 100644 --- a/tests/test_apps.py +++ b/tests/test_apps.py @@ -11,6 +11,10 @@ def test_ready(self, settings): app = S3FileConfig("s3file", importlib.import_module("tests.testapp")) app.ready() assert not isinstance(forms.ClearableFileInput(), S3FileInputMixin) - settings.DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" + settings.STORAGES = { + **settings.STORAGES, + "DEFAULT": {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}, + } + app.ready() assert isinstance(forms.ClearableFileInput(), S3FileInputMixin) diff --git a/tests/test_forms.py b/tests/test_forms.py index 9f4ee39..c2d33ba 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -11,11 +11,67 @@ from selenium.webdriver.support.expected_conditions import staleness_of from selenium.webdriver.support.wait import WebDriverWait +from s3file import forms from s3file.storages import storage from tests.testapp.forms import FileForm from tests.testapp.models import FileModel +class TestAsset: + def test_init(self): + asset = forms.Asset("path") + assert asset.path == "path" + + def test_eq(self): + asset = forms.Asset("path") + assert asset == "path" + assert asset == forms.Asset("path") + assert asset != forms.Asset("other") + + def test_hash(self): + asset = forms.Asset("path") + assert hash(asset) == hash("path") + + def test_str(self, settings): + settings.STORAGES = { + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage" + }, + } + asset = forms.Asset("path") + assert str(asset) == "/static/path" + + def test_absolute_path(self, settings): + settings.STORAGES = { + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage" + }, + } + asset = forms.Asset("path") + assert asset.absolute_path("path") == "/static/path" + assert asset.absolute_path("/path") == "/path" + assert asset.absolute_path("http://path") == "http://path" + assert asset.absolute_path("https://path") == "https://path" + + def test_repr(self): + asset = forms.Asset("path") + assert repr(asset) == "Asset: 'path'" + + +class TestESM: + def test_str(self, settings): + settings.STORAGES = { + "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"}, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage" + }, + } + js = forms.ESM("path") + assert str(js) == '' + + @contextmanager def wait_for_page_load(driver, timeout=30): old_page = driver.find_element(By.TAG_NAME, "html") @@ -71,7 +127,7 @@ def test_clear(self, filemodel): def test_build_attr(self, freeze_upload_folder): assert set(ClearableFileInput().build_attrs({}).keys()) == { - "class", + "is", "data-url", "data-fields-x-amz-algorithm", "data-fields-x-amz-date", @@ -85,11 +141,7 @@ def test_build_attr(self, freeze_upload_folder): ClearableFileInput().build_attrs({})["data-s3f-signature"] == "VRIPlI1LCjUh1EtplrgxQrG8gSAaIwT48mMRlwaCytI" ) - assert ClearableFileInput().build_attrs({})["class"] == "s3file" - assert ( - ClearableFileInput().build_attrs({"class": "my-class"})["class"] - == "my-class s3file" - ) + assert ClearableFileInput().build_attrs({})["is"] == "s3-file" def test_get_conditions(self, freeze_upload_folder): conditions = ClearableFileInput().get_conditions(None) @@ -179,15 +231,6 @@ def test_file_update( def test_file_insert_submit_value( self, driver, live_server, upload_file, freeze_upload_folder ): - driver.get(live_server + self.create_url) - file_input = driver.find_element(By.XPATH, "//input[@name='file']") - file_input.send_keys(upload_file) - assert file_input.get_attribute("name") == "file" - save_button = driver.find_element(By.XPATH, "//input[@name='save']") - with wait_for_page_load(driver, timeout=10): - save_button.click() - assert "save" in driver.page_source - driver.get(live_server + self.create_url) file_input = driver.find_element(By.XPATH, "//input[@name='file']") file_input.send_keys(upload_file) @@ -199,25 +242,38 @@ def test_file_insert_submit_value( assert "continue_value" in driver.page_source @pytest.mark.selenium - def test_progress(self, driver, live_server, upload_file, freeze_upload_folder): + def test_file_insert_submit_formaction( + self, driver, live_server, upload_file, freeze_upload_folder + ): driver.get(live_server + self.create_url) file_input = driver.find_element(By.XPATH, "//input[@name='file']") file_input.send_keys(upload_file) assert file_input.get_attribute("name") == "file" - save_button = driver.find_element(By.XPATH, "//input[@name='save']") + save_button = driver.find_element(By.XPATH, "//button[@name='custom_save']") with wait_for_page_load(driver, timeout=10): save_button.click() - assert "save" in driver.page_source + assert "custom_save" in driver.page_source + assert "custom_target" in driver.page_source + assert "foo" in driver.page_source + assert "bar" in driver.page_source + @pytest.mark.selenium + def test_file_insert_change_event( + self, + driver, + live_server, + upload_file, + another_upload_file, + freeze_upload_folder, + ): driver.get(live_server + self.create_url) file_input = driver.find_element(By.XPATH, "//input[@name='file']") file_input.send_keys(upload_file) - assert file_input.get_attribute("name") == "file" - save_button = driver.find_element(By.XPATH, "//button[@name='save_continue']") + file_input.send_keys(another_upload_file) + save_button = driver.find_element(By.CSS_SELECTOR, "input[name=save]") with wait_for_page_load(driver, timeout=10): save_button.click() - response = json.loads(driver.find_elements(By.CSS_SELECTOR, "pre")[0].text) - assert response["POST"]["progress"] == "1" + assert "save" in driver.page_source @pytest.mark.selenium def test_multi_file( diff --git a/tests/testapp/templates/form.html b/tests/testapp/templates/form.html index 7891e93..f8af819 100644 --- a/tests/testapp/templates/form.html +++ b/tests/testapp/templates/form.html @@ -1,10 +1,10 @@ {% load static %} - + - {##} + Form @@ -16,33 +16,8 @@ {{ form }} -
-
0% -
-
+ {{ form.media.js }} - + diff --git a/tests/testapp/views.py b/tests/testapp/views.py index 85e8f54..097147c 100644 --- a/tests/testapp/views.py +++ b/tests/testapp/views.py @@ -21,6 +21,7 @@ class ExampleCreateView(generic.CreateView): def form_valid(self, form): return JsonResponse( { + "GET": self.request.GET, "POST": self.request.POST, "FILES": { "file": self.request.FILES.getlist("file"),