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 17e4acb..d6db0c8 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
-
-```
-
-```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 }}
-
+
{{ 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"),