ci: merge main to release (pull request #7034 from ietf-tools/main)
ci: merge main to release
This commit is contained in:
commit
ac4062f921
57
.github/workflows/build.yml
vendored
57
.github/workflows/build.yml
vendored
|
@ -123,6 +123,9 @@ jobs:
|
|||
if: ${{ !failure() && !cancelled() }}
|
||||
needs: [tests, prepare]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
env:
|
||||
SHOULD_DEPLOY: ${{needs.prepare.outputs.should_deploy}}
|
||||
PKG_VERSION: ${{needs.prepare.outputs.pkg_version}}
|
||||
|
@ -154,13 +157,17 @@ jobs:
|
|||
- name: Make Release Build
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
BROWSERSLIST_IGNORE_OLD_DATA: 1
|
||||
run: |
|
||||
echo "PKG_VERSION: $PKG_VERSION"
|
||||
echo "GITHUB_SHA: $GITHUB_SHA"
|
||||
echo "GITHUB_REF_NAME: $GITHUB_REF_NAME"
|
||||
echo "Running build script..."
|
||||
chmod +x ./dev/deploy/build.sh
|
||||
sh ./dev/deploy/build.sh
|
||||
echo "Running frontend build script..."
|
||||
echo "Compiling native node packages..."
|
||||
yarn rebuild
|
||||
echo "Packaging static assets..."
|
||||
yarn build --base=https://static.ietf.org/dt/$PKG_VERSION/
|
||||
yarn legacy:build
|
||||
echo "Setting version $PKG_VERSION..."
|
||||
sed -i -r -e "s|^__version__ += '.*'$|__version__ = '$PKG_VERSION'|" ietf/__init__.py
|
||||
sed -i -r -e "s|^__release_hash__ += '.*'$|__release_hash__ = '$GITHUB_SHA'|" ietf/__init__.py
|
||||
|
@ -178,7 +185,7 @@ jobs:
|
|||
run: |
|
||||
echo "Build release tarball..."
|
||||
mkdir -p /home/runner/work/release
|
||||
tar -czf /home/runner/work/release/release.tar.gz -X dev/deploy/exclude-patterns.txt .
|
||||
tar -czf /home/runner/work/release/release.tar.gz -X dev/build/exclude-patterns.txt .
|
||||
|
||||
- name: Collect + Push Statics
|
||||
env:
|
||||
|
@ -189,11 +196,47 @@ jobs:
|
|||
AWS_ENDPOINT_URL: ${{ secrets.CF_R2_ENDPOINT }}
|
||||
run: |
|
||||
echo "Collecting statics..."
|
||||
docker run --rm --name collectstatics -v $(pwd):/workspace ghcr.io/ietf-tools/datatracker-app-base:latest sh dev/deploy/collectstatics.sh
|
||||
docker run --rm --name collectstatics -v $(pwd):/workspace ghcr.io/ietf-tools/datatracker-app-base:latest sh dev/build/collectstatics.sh
|
||||
echo "Pushing statics..."
|
||||
cd static
|
||||
aws s3 sync . s3://static/dt/$PKG_VERSION --only-show-errors
|
||||
|
||||
- name: Augment dockerignore for docker image build
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
cat >> .dockerignore <<EOL
|
||||
.devcontainer
|
||||
.github
|
||||
.vscode
|
||||
helm
|
||||
playwright
|
||||
svn-history
|
||||
docker-compose.yml
|
||||
EOL
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Release Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: dev/build/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/ietf-tools/datatracker:${{ env.PKG_VERSION }}
|
||||
|
||||
- name: Update CHANGELOG
|
||||
id: changelog
|
||||
uses: Requarks/changelog-action@v1
|
||||
|
@ -269,7 +312,7 @@ jobs:
|
|||
steps:
|
||||
- name: Notify on Slack (Success)
|
||||
if: ${{ !contains(join(needs.*.result, ','), 'failure') }}
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
channel-id: ${{ secrets.SLACK_GH_BUILDS_CHANNEL_ID }}
|
||||
payload: |
|
||||
|
@ -292,7 +335,7 @@ jobs:
|
|||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_GH_BOT }}
|
||||
- name: Notify on Slack (Failure)
|
||||
if: ${{ contains(join(needs.*.result, ','), 'failure') }}
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
uses: slackapi/slack-github-action@v1.25.0
|
||||
with:
|
||||
channel-id: ${{ secrets.SLACK_GH_BUILDS_CHANNEL_ID }}
|
||||
payload: |
|
||||
|
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
@ -17,6 +17,6 @@ jobs:
|
|||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
vulnerability-check: false
|
||||
|
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -59,7 +59,7 @@ jobs:
|
|||
path: geckodriver.log
|
||||
|
||||
- name: Upload Coverage Results to Codecov
|
||||
uses: codecov/codecov-action@v3.1.4
|
||||
uses: codecov/codecov-action@v3.1.5
|
||||
with:
|
||||
files: coverage.xml
|
||||
|
||||
|
|
198
.pnp.cjs
generated
198
.pnp.cjs
generated
|
@ -54,7 +54,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["browser-fs-access", "npm:0.35.0"],\
|
||||
["browserlist", "npm:1.0.1"],\
|
||||
["c8", "npm:9.1.0"],\
|
||||
["caniuse-lite", "npm:1.0.30001576"],\
|
||||
["caniuse-lite", "npm:1.0.30001581"],\
|
||||
["d3", "npm:7.8.5"],\
|
||||
["eslint", "npm:8.56.0"],\
|
||||
["eslint-config-standard", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:17.1.0"],\
|
||||
|
@ -84,7 +84,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["pinia", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.1.7"],\
|
||||
["pinia-plugin-persist", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:1.0.0"],\
|
||||
["pug", "npm:3.0.2"],\
|
||||
["sass", "npm:1.69.7"],\
|
||||
["sass", "npm:1.70.0"],\
|
||||
["seedrandom", "npm:3.0.5"],\
|
||||
["select2", "npm:4.1.0-rc.0"],\
|
||||
["select2-bootstrap-5-theme", "npm:1.3.0"],\
|
||||
|
@ -93,8 +93,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["slugify", "npm:1.6.6"],\
|
||||
["sortablejs", "npm:1.15.2"],\
|
||||
["vanillajs-datepicker", "npm:1.3.4"],\
|
||||
["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.1"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"],\
|
||||
["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"],\
|
||||
["vue-router", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.2.5"],\
|
||||
["zxcvbn", "npm:4.4.2"]\
|
||||
],\
|
||||
|
@ -231,7 +231,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
"packageDependencies": [\
|
||||
["@css-render/vue3-ssr", "virtual:535ce3a5bf8429bbdd476b0f4bedb68cb91a1d57eac35720679464b7eeafc062414751fda54be317bf7e7886eec3b33992730a480671dc4d6974fd45406b1082#npm:0.15.10"],\
|
||||
["@types/vue", null],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
@ -244,7 +244,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
"packageDependencies": [\
|
||||
["@css-render/vue3-ssr", "virtual:9083f0b60f7ff3c9457189a27c2996ceed17cab3520ae1c32ab5e5244b992c3c8baaf999ad3c2b19ef13e1964e3197201ef68b1b3153ac72686293207b8892cf#npm:0.15.12"],\
|
||||
["@types/vue", null],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
@ -744,7 +744,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["@fullcalendar/core", "npm:6.1.10"],\
|
||||
["@types/fullcalendar__core", null],\
|
||||
["@types/vue", null],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@fullcalendar/core",\
|
||||
|
@ -2707,8 +2707,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["@vitejs/plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.6.2"],\
|
||||
["@types/vite", null],\
|
||||
["@types/vue", null],\
|
||||
["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.1"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vite",\
|
||||
|
@ -2720,12 +2720,12 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
}]\
|
||||
]],\
|
||||
["@vue/compiler-core", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-compiler-core-npm-3.4.13-acb31588b3-5f486b5ca8.zip/node_modules/@vue/compiler-core/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-compiler-core-npm-3.4.15-4f131dda24-1610f715b8.zip/node_modules/@vue/compiler-core/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/compiler-core", "npm:3.4.13"],\
|
||||
["@vue/compiler-core", "npm:3.4.15"],\
|
||||
["@babel/parser", "npm:7.23.6"],\
|
||||
["@vue/shared", "npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.15"],\
|
||||
["entities", "npm:4.5.0"],\
|
||||
["estree-walker", "npm:2.0.2"],\
|
||||
["source-map-js", "npm:1.0.2"]\
|
||||
|
@ -2734,41 +2734,41 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
}]\
|
||||
]],\
|
||||
["@vue/compiler-dom", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-compiler-dom-npm-3.4.13-419b24ea95-2afdacc038.zip/node_modules/@vue/compiler-dom/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-compiler-dom-npm-3.4.15-8299b45d96-373968c2c6.zip/node_modules/@vue/compiler-dom/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/compiler-dom", "npm:3.4.13"],\
|
||||
["@vue/compiler-core", "npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.13"]\
|
||||
["@vue/compiler-dom", "npm:3.4.15"],\
|
||||
["@vue/compiler-core", "npm:3.4.15"],\
|
||||
["@vue/shared", "npm:3.4.15"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@vue/compiler-sfc", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-compiler-sfc-npm-3.4.13-5fd28e3447-9252b9f10c.zip/node_modules/@vue/compiler-sfc/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-compiler-sfc-npm-3.4.15-3d3ce9fc16-4a707346c3.zip/node_modules/@vue/compiler-sfc/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/compiler-sfc", "npm:3.4.13"],\
|
||||
["@vue/compiler-sfc", "npm:3.4.15"],\
|
||||
["@babel/parser", "npm:7.23.6"],\
|
||||
["@vue/compiler-core", "npm:3.4.13"],\
|
||||
["@vue/compiler-dom", "npm:3.4.13"],\
|
||||
["@vue/compiler-ssr", "npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.13"],\
|
||||
["@vue/compiler-core", "npm:3.4.15"],\
|
||||
["@vue/compiler-dom", "npm:3.4.15"],\
|
||||
["@vue/compiler-ssr", "npm:3.4.15"],\
|
||||
["@vue/shared", "npm:3.4.15"],\
|
||||
["estree-walker", "npm:2.0.2"],\
|
||||
["magic-string", "npm:0.30.5"],\
|
||||
["postcss", "npm:8.4.32"],\
|
||||
["postcss", "npm:8.4.33"],\
|
||||
["source-map-js", "npm:1.0.2"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@vue/compiler-ssr", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-compiler-ssr-npm-3.4.13-f1c98d5a6b-99fae88e13.zip/node_modules/@vue/compiler-ssr/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-compiler-ssr-npm-3.4.15-05dd3d13a5-45a12ae2dd.zip/node_modules/@vue/compiler-ssr/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/compiler-ssr", "npm:3.4.13"],\
|
||||
["@vue/compiler-dom", "npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.13"]\
|
||||
["@vue/compiler-ssr", "npm:3.4.15"],\
|
||||
["@vue/compiler-dom", "npm:3.4.15"],\
|
||||
["@vue/shared", "npm:3.4.15"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
|
@ -2783,54 +2783,54 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
}]\
|
||||
]],\
|
||||
["@vue/reactivity", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-reactivity-npm-3.4.13-ad954039b3-883ba2fb31.zip/node_modules/@vue/reactivity/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-reactivity-npm-3.4.15-fde29aa046-e1f8ef7ec3.zip/node_modules/@vue/reactivity/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/reactivity", "npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.13"]\
|
||||
["@vue/reactivity", "npm:3.4.15"],\
|
||||
["@vue/shared", "npm:3.4.15"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@vue/runtime-core", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-runtime-core-npm-3.4.13-a92d1fdb22-196c6c894d.zip/node_modules/@vue/runtime-core/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-runtime-core-npm-3.4.15-b9057fef14-6ab6721410.zip/node_modules/@vue/runtime-core/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/runtime-core", "npm:3.4.13"],\
|
||||
["@vue/reactivity", "npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.13"]\
|
||||
["@vue/runtime-core", "npm:3.4.15"],\
|
||||
["@vue/reactivity", "npm:3.4.15"],\
|
||||
["@vue/shared", "npm:3.4.15"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@vue/runtime-dom", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-runtime-dom-npm-3.4.13-b9f911a017-8811687c23.zip/node_modules/@vue/runtime-dom/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-runtime-dom-npm-3.4.15-7dfc9b71f4-4f2e79d956.zip/node_modules/@vue/runtime-dom/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/runtime-dom", "npm:3.4.13"],\
|
||||
["@vue/runtime-core", "npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.13"],\
|
||||
["@vue/runtime-dom", "npm:3.4.15"],\
|
||||
["@vue/runtime-core", "npm:3.4.15"],\
|
||||
["@vue/shared", "npm:3.4.15"],\
|
||||
["csstype", "npm:3.1.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@vue/server-renderer", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-server-renderer-npm-3.4.13-6a75a1f39c-f17fff6af2.zip/node_modules/@vue/server-renderer/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-server-renderer-npm-3.4.15-fd81b21d4f-de93ccffe7.zip/node_modules/@vue/server-renderer/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/server-renderer", "npm:3.4.13"]\
|
||||
["@vue/server-renderer", "npm:3.4.15"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:70f0bf839f9a0f4ee48fb830cbf74c97964359640d4a604894b35bec6c5aed7fce5a0f06c521ced0640b9b3ca30c78af522cffa59c4405403ad1ab0de99f65b1#npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@vue-server-renderer-virtual-a38b412c5f/0/cache/@vue-server-renderer-npm-3.4.13-6a75a1f39c-f17fff6af2.zip/node_modules/@vue/server-renderer/",\
|
||||
["virtual:22db5c00fc66102c519417539d30aa289c17b3734eaa2f7ecaa126181d222b35d01ed6523b175bd3a8e0b244322f685e795d810e50df8db08ab29d82533296a8#npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/__virtual__/@vue-server-renderer-virtual-66d8b02a0a/0/cache/@vue-server-renderer-npm-3.4.15-fd81b21d4f-de93ccffe7.zip/node_modules/@vue/server-renderer/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/server-renderer", "virtual:70f0bf839f9a0f4ee48fb830cbf74c97964359640d4a604894b35bec6c5aed7fce5a0f06c521ced0640b9b3ca30c78af522cffa59c4405403ad1ab0de99f65b1#npm:3.4.13"],\
|
||||
["@vue/server-renderer", "virtual:22db5c00fc66102c519417539d30aa289c17b3734eaa2f7ecaa126181d222b35d01ed6523b175bd3a8e0b244322f685e795d810e50df8db08ab29d82533296a8#npm:3.4.15"],\
|
||||
["@types/vue", null],\
|
||||
["@vue/compiler-ssr", "npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.13"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["@vue/compiler-ssr", "npm:3.4.15"],\
|
||||
["@vue/shared", "npm:3.4.15"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
@ -2840,10 +2840,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
}]\
|
||||
]],\
|
||||
["@vue/shared", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-shared-npm-3.4.13-4dcbacd500-c514944886.zip/node_modules/@vue/shared/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/@vue-shared-npm-3.4.15-638dcb7e89-237db3a880.zip/node_modules/@vue/shared/",\
|
||||
"packageDependencies": [\
|
||||
["@vue/shared", "npm:3.4.13"]\
|
||||
["@vue/shared", "npm:3.4.15"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
|
@ -3443,10 +3443,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.0.30001576", {\
|
||||
"packageLocation": "./.yarn/cache/caniuse-lite-npm-1.0.30001576-3d0983cdce-b8b332675f.zip/node_modules/caniuse-lite/",\
|
||||
["npm:1.0.30001581", {\
|
||||
"packageLocation": "./.yarn/cache/caniuse-lite-npm-1.0.30001581-7909cc6e66-ca4e2cd9d0.zip/node_modules/caniuse-lite/",\
|
||||
"packageDependencies": [\
|
||||
["caniuse-lite", "npm:1.0.30001576"]\
|
||||
["caniuse-lite", "npm:1.0.30001581"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
|
@ -7194,7 +7194,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["treemate", "npm:0.3.11"],\
|
||||
["vdirs", "virtual:9083f0b60f7ff3c9457189a27c2996ceed17cab3520ae1c32ab5e5244b992c3c8baaf999ad3c2b19ef13e1964e3197201ef68b1b3153ac72686293207b8892cf#npm:0.1.8"],\
|
||||
["vooks", "virtual:9083f0b60f7ff3c9457189a27c2996ceed17cab3520ae1c32ab5e5244b992c3c8baaf999ad3c2b19ef13e1964e3197201ef68b1b3153ac72686293207b8892cf#npm:0.2.12"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"],\
|
||||
["vueuc", "virtual:9083f0b60f7ff3c9457189a27c2996ceed17cab3520ae1c32ab5e5244b992c3c8baaf999ad3c2b19ef13e1964e3197201ef68b1b3153ac72686293207b8892cf#npm:0.4.58"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
|
@ -7690,7 +7690,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["@vue/composition-api", null],\
|
||||
["@vue/devtools-api", "npm:6.5.0"],\
|
||||
["typescript", null],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"],\
|
||||
["vue-demi", "virtual:cf6f7439ee76dfd2e7f8f2565ae847d76901434fc49c65702190cdf3d1c61e61c701a5c45b514c4bdeacb8f4bcac9c8a98bd4db3d0bc8e403d9e8db2cf14372a#npm:0.14.5"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
|
@ -7721,7 +7721,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["@types/vue__composition-api", null],\
|
||||
["@vue/composition-api", null],\
|
||||
["pinia", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.1.7"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"],\
|
||||
["vue-demi", "virtual:f56fcf19bbebc2ada1b28955da8cc216b1e9a569a1a7337d2d1926c1ebd1bc7a5bd91aedae1d05c15c8562f33caf7c59bd3020a667340f6bdc6a7b13fc2ba847#npm:0.12.5"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
|
@ -7736,16 +7736,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
}]\
|
||||
]],\
|
||||
["postcss", [\
|
||||
["npm:8.4.32", {\
|
||||
"packageLocation": "./.yarn/cache/postcss-npm-8.4.32-2004ba88b8-220d9d0bf5.zip/node_modules/postcss/",\
|
||||
"packageDependencies": [\
|
||||
["postcss", "npm:8.4.32"],\
|
||||
["nanoid", "npm:3.3.7"],\
|
||||
["picocolors", "npm:1.0.0"],\
|
||||
["source-map-js", "npm:1.0.2"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:8.4.33", {\
|
||||
"packageLocation": "./.yarn/cache/postcss-npm-8.4.33-6ba8157009-6f98b2af4b.zip/node_modules/postcss/",\
|
||||
"packageDependencies": [\
|
||||
|
@ -8275,7 +8265,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["browser-fs-access", "npm:0.35.0"],\
|
||||
["browserlist", "npm:1.0.1"],\
|
||||
["c8", "npm:9.1.0"],\
|
||||
["caniuse-lite", "npm:1.0.30001576"],\
|
||||
["caniuse-lite", "npm:1.0.30001581"],\
|
||||
["d3", "npm:7.8.5"],\
|
||||
["eslint", "npm:8.56.0"],\
|
||||
["eslint-config-standard", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:17.1.0"],\
|
||||
|
@ -8305,7 +8295,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["pinia", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.1.7"],\
|
||||
["pinia-plugin-persist", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:1.0.0"],\
|
||||
["pug", "npm:3.0.2"],\
|
||||
["sass", "npm:1.69.7"],\
|
||||
["sass", "npm:1.70.0"],\
|
||||
["seedrandom", "npm:3.0.5"],\
|
||||
["select2", "npm:4.1.0-rc.0"],\
|
||||
["select2-bootstrap-5-theme", "npm:1.3.0"],\
|
||||
|
@ -8314,8 +8304,8 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["slugify", "npm:1.6.6"],\
|
||||
["sortablejs", "npm:1.15.2"],\
|
||||
["vanillajs-datepicker", "npm:1.3.4"],\
|
||||
["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.1"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"],\
|
||||
["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"],\
|
||||
["vue-router", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.2.5"],\
|
||||
["zxcvbn", "npm:4.4.2"]\
|
||||
],\
|
||||
|
@ -8402,10 +8392,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
],\
|
||||
"linkType": "HARD"\
|
||||
}],\
|
||||
["npm:1.69.7", {\
|
||||
"packageLocation": "./.yarn/cache/sass-npm-1.69.7-ac434a094c-c67cd32b69.zip/node_modules/sass/",\
|
||||
["npm:1.70.0", {\
|
||||
"packageLocation": "./.yarn/cache/sass-npm-1.70.0-153257249c-fd1b622cf9.zip/node_modules/sass/",\
|
||||
"packageDependencies": [\
|
||||
["sass", "npm:1.69.7"],\
|
||||
["sass", "npm:1.70.0"],\
|
||||
["chokidar", "npm:3.5.3"],\
|
||||
["immutable", "npm:4.0.0"],\
|
||||
["source-map-js", "npm:1.0.2"]\
|
||||
|
@ -9186,7 +9176,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["vdirs", "virtual:9083f0b60f7ff3c9457189a27c2996ceed17cab3520ae1c32ab5e5244b992c3c8baaf999ad3c2b19ef13e1964e3197201ef68b1b3153ac72686293207b8892cf#npm:0.1.8"],\
|
||||
["@types/vue", null],\
|
||||
["evtd", "npm:0.2.3"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
@ -9196,17 +9186,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
}]\
|
||||
]],\
|
||||
["vite", [\
|
||||
["npm:4.5.1", {\
|
||||
"packageLocation": "./.yarn/cache/vite-npm-4.5.1-567bbcf9ff-72b3584b3d.zip/node_modules/vite/",\
|
||||
["npm:4.5.2", {\
|
||||
"packageLocation": "./.yarn/cache/vite-npm-4.5.2-e430b2c117-9d1f84f703.zip/node_modules/vite/",\
|
||||
"packageDependencies": [\
|
||||
["vite", "npm:4.5.1"]\
|
||||
["vite", "npm:4.5.2"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.1", {\
|
||||
"packageLocation": "./.yarn/__virtual__/vite-virtual-a00c7b893c/0/cache/vite-npm-4.5.1-567bbcf9ff-72b3584b3d.zip/node_modules/vite/",\
|
||||
["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2", {\
|
||||
"packageLocation": "./.yarn/__virtual__/vite-virtual-8f548b7c00/0/cache/vite-npm-4.5.2-e430b2c117-9d1f84f703.zip/node_modules/vite/",\
|
||||
"packageDependencies": [\
|
||||
["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.1"],\
|
||||
["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2"],\
|
||||
["@types/less", null],\
|
||||
["@types/lightningcss", null],\
|
||||
["@types/node", null],\
|
||||
|
@ -9220,7 +9210,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["lightningcss", null],\
|
||||
["postcss", "npm:8.4.33"],\
|
||||
["rollup", "npm:3.29.4"],\
|
||||
["sass", "npm:1.69.7"],\
|
||||
["sass", "npm:1.70.0"],\
|
||||
["stylus", null],\
|
||||
["sugarss", null],\
|
||||
["terser", null]\
|
||||
|
@ -9266,7 +9256,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["vooks", "virtual:9083f0b60f7ff3c9457189a27c2996ceed17cab3520ae1c32ab5e5244b992c3c8baaf999ad3c2b19ef13e1964e3197201ef68b1b3153ac72686293207b8892cf#npm:0.2.12"],\
|
||||
["@types/vue", null],\
|
||||
["evtd", "npm:0.2.3"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
@ -9276,23 +9266,23 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
}]\
|
||||
]],\
|
||||
["vue", [\
|
||||
["npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/cache/vue-npm-3.4.13-668436a4a7-c9f8edf5fc.zip/node_modules/vue/",\
|
||||
["npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/cache/vue-npm-3.4.15-11fe9fcc84-6e9ff02c9b.zip/node_modules/vue/",\
|
||||
"packageDependencies": [\
|
||||
["vue", "npm:3.4.13"]\
|
||||
["vue", "npm:3.4.15"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13", {\
|
||||
"packageLocation": "./.yarn/__virtual__/vue-virtual-70f0bf839f/0/cache/vue-npm-3.4.13-668436a4a7-c9f8edf5fc.zip/node_modules/vue/",\
|
||||
["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15", {\
|
||||
"packageLocation": "./.yarn/__virtual__/vue-virtual-22db5c00fc/0/cache/vue-npm-3.4.15-11fe9fcc84-6e9ff02c9b.zip/node_modules/vue/",\
|
||||
"packageDependencies": [\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"],\
|
||||
["@types/typescript", null],\
|
||||
["@vue/compiler-dom", "npm:3.4.13"],\
|
||||
["@vue/compiler-sfc", "npm:3.4.13"],\
|
||||
["@vue/runtime-dom", "npm:3.4.13"],\
|
||||
["@vue/server-renderer", "virtual:70f0bf839f9a0f4ee48fb830cbf74c97964359640d4a604894b35bec6c5aed7fce5a0f06c521ced0640b9b3ca30c78af522cffa59c4405403ad1ab0de99f65b1#npm:3.4.13"],\
|
||||
["@vue/shared", "npm:3.4.13"],\
|
||||
["@vue/compiler-dom", "npm:3.4.15"],\
|
||||
["@vue/compiler-sfc", "npm:3.4.15"],\
|
||||
["@vue/runtime-dom", "npm:3.4.15"],\
|
||||
["@vue/server-renderer", "virtual:22db5c00fc66102c519417539d30aa289c17b3734eaa2f7ecaa126181d222b35d01ed6523b175bd3a8e0b244322f685e795d810e50df8db08ab29d82533296a8#npm:3.4.15"],\
|
||||
["@vue/shared", "npm:3.4.15"],\
|
||||
["typescript", null]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
|
@ -9324,7 +9314,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["@types/vue", null],\
|
||||
["@types/vue__composition-api", null],\
|
||||
["@vue/composition-api", null],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
@ -9341,7 +9331,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["@types/vue", null],\
|
||||
["@types/vue__composition-api", null],\
|
||||
["@vue/composition-api", null],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
@ -9395,7 +9385,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["vue-router", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.2.5"],\
|
||||
["@types/vue", null],\
|
||||
["@vue/devtools-api", "npm:6.5.0"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
@ -9424,7 +9414,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
|
|||
["seemly", "npm:0.3.6"],\
|
||||
["vdirs", "virtual:9083f0b60f7ff3c9457189a27c2996ceed17cab3520ae1c32ab5e5244b992c3c8baaf999ad3c2b19ef13e1964e3197201ef68b1b3153ac72686293207b8892cf#npm:0.1.8"],\
|
||||
["vooks", "virtual:9083f0b60f7ff3c9457189a27c2996ceed17cab3520ae1c32ab5e5244b992c3c8baaf999ad3c2b19ef13e1964e3197201ef68b1b3153ac72686293207b8892cf#npm:0.2.12"],\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.13"]\
|
||||
["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.15"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/vue",\
|
||||
|
|
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-core-npm-3.4.15-4f131dda24-1610f715b8.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-core-npm-3.4.15-4f131dda24-1610f715b8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-dom-npm-3.4.15-8299b45d96-373968c2c6.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-dom-npm-3.4.15-8299b45d96-373968c2c6.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-sfc-npm-3.4.15-3d3ce9fc16-4a707346c3.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-sfc-npm-3.4.15-3d3ce9fc16-4a707346c3.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-ssr-npm-3.4.15-05dd3d13a5-45a12ae2dd.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-ssr-npm-3.4.15-05dd3d13a5-45a12ae2dd.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-reactivity-npm-3.4.15-fde29aa046-e1f8ef7ec3.zip
vendored
Normal file
BIN
.yarn/cache/@vue-reactivity-npm-3.4.15-fde29aa046-e1f8ef7ec3.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-runtime-core-npm-3.4.15-b9057fef14-6ab6721410.zip
vendored
Normal file
BIN
.yarn/cache/@vue-runtime-core-npm-3.4.15-b9057fef14-6ab6721410.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-runtime-dom-npm-3.4.15-7dfc9b71f4-4f2e79d956.zip
vendored
Normal file
BIN
.yarn/cache/@vue-runtime-dom-npm-3.4.15-7dfc9b71f4-4f2e79d956.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-server-renderer-npm-3.4.15-fd81b21d4f-de93ccffe7.zip
vendored
Normal file
BIN
.yarn/cache/@vue-server-renderer-npm-3.4.15-fd81b21d4f-de93ccffe7.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-shared-npm-3.4.15-638dcb7e89-237db3a880.zip
vendored
Normal file
BIN
.yarn/cache/@vue-shared-npm-3.4.15-638dcb7e89-237db3a880.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/caniuse-lite-npm-1.0.30001581-7909cc6e66-ca4e2cd9d0.zip
vendored
Normal file
BIN
.yarn/cache/caniuse-lite-npm-1.0.30001581-7909cc6e66-ca4e2cd9d0.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/sass-npm-1.70.0-153257249c-fd1b622cf9.zip
vendored
Normal file
BIN
.yarn/cache/sass-npm-1.70.0-153257249c-fd1b622cf9.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/vue-npm-3.4.13-668436a4a7-c9f8edf5fc.zip
vendored
BIN
.yarn/cache/vue-npm-3.4.13-668436a4a7-c9f8edf5fc.zip
vendored
Binary file not shown.
BIN
.yarn/cache/vue-npm-3.4.15-11fe9fcc84-6e9ff02c9b.zip
vendored
Normal file
BIN
.yarn/cache/vue-npm-3.4.15-11fe9fcc84-6e9ff02c9b.zip
vendored
Normal file
Binary file not shown.
|
@ -45,6 +45,7 @@ ID=/a/ietfdata/doc/draft/repository
|
|||
DERIVED=/a/ietfdata/derived
|
||||
DOWNLOAD=/a/www/www6s/download
|
||||
|
||||
## Start of script refactored into idindex_update_task() ===
|
||||
export TMPDIR=/a/tmp
|
||||
|
||||
TMPFILE1=`mktemp` || exit 1
|
||||
|
@ -85,6 +86,8 @@ mv $TMPFILE9 $DERIVED/1id-index.txt
|
|||
mv $TMPFILEA $DERIVED/1id-abstracts.txt
|
||||
mv $TMPFILEB $DERIVED/all_id2.txt
|
||||
|
||||
## End of script refactored into idindex_update_task() ===
|
||||
|
||||
$DTDIR/ietf/manage.py generate_idnits2_rfc_status
|
||||
$DTDIR/ietf/manage.py generate_idnits2_rfcs_obsoleted
|
||||
|
||||
|
|
17
dev/build/Dockerfile
Normal file
17
dev/build/Dockerfile
Normal file
|
@ -0,0 +1,17 @@
|
|||
FROM ghcr.io/ietf-tools/datatracker-app-base:latest
|
||||
LABEL maintainer="IETF Tools Team <tools-discuss@ietf.org>"
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
COPY . .
|
||||
COPY ./dev/build/start.sh ./start.sh
|
||||
RUN pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt
|
||||
RUN chmod +x start.sh && \
|
||||
chmod +x docker/scripts/app-create-dirs.sh && \
|
||||
sh ./docker/scripts/app-create-dirs.sh
|
||||
|
||||
VOLUME [ "/assets" ]
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["./start.sh"]
|
13
dev/build/collectstatics.sh
Normal file
13
dev/build/collectstatics.sh
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Copy temp local settings
|
||||
cp dev/build/settings_local_collectstatics.py ietf/settings_local.py
|
||||
|
||||
# Install Python dependencies
|
||||
pip --disable-pip-version-check --no-cache-dir install -r requirements.txt
|
||||
|
||||
# Collect statics
|
||||
ietf/manage.py collectstatic
|
||||
|
||||
# Delete temp local settings
|
||||
rm ietf/settings_local.py
|
10
dev/build/start.sh
Normal file
10
dev/build/start.sh
Normal file
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Running Datatracker checks..."
|
||||
./ietf/manage.py check
|
||||
|
||||
echo "Running Datatracker migrations..."
|
||||
./ietf/manage.py migrate --settings=settings_local
|
||||
|
||||
echo "Starting Datatracker..."
|
||||
./ietf/manage.py runserver 0.0.0.0:8000 --settings=settings_local
|
14
dev/coverage-action/package-lock.json
generated
14
dev/coverage-action/package-lock.json
generated
|
@ -22,7 +22,7 @@
|
|||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"npm-check-updates": "16.14.12"
|
||||
"npm-check-updates": "16.14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
@ -4079,9 +4079,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/npm-check-updates": {
|
||||
"version": "16.14.12",
|
||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.12.tgz",
|
||||
"integrity": "sha512-5FvqaDX8AqWWTDQFbBllgLwoRXTvzlqVIRSKl9Kg8bYZTfNwMnrp1Zlmb5e/ocf11UjPTc+ShBFjYQ7kg6FL0w==",
|
||||
"version": "16.14.14",
|
||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.14.tgz",
|
||||
"integrity": "sha512-Y3ajS/Ep40jM489rLBdz9jehn/BMil5s9fA4PSr2ZJxxSmtLWCSmRqsI2IEZ9Nb3MTMu8a3s7kBs0l+JbjdkTA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
|
@ -9249,9 +9249,9 @@
|
|||
}
|
||||
},
|
||||
"npm-check-updates": {
|
||||
"version": "16.14.12",
|
||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.12.tgz",
|
||||
"integrity": "sha512-5FvqaDX8AqWWTDQFbBllgLwoRXTvzlqVIRSKl9Kg8bYZTfNwMnrp1Zlmb5e/ocf11UjPTc+ShBFjYQ7kg6FL0w==",
|
||||
"version": "16.14.14",
|
||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.14.tgz",
|
||||
"integrity": "sha512-Y3ajS/Ep40jM489rLBdz9jehn/BMil5s9fA4PSr2ZJxxSmtLWCSmRqsI2IEZ9Nb3MTMu8a3s7kBs0l+JbjdkTA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^5.3.0",
|
||||
|
|
|
@ -19,6 +19,6 @@
|
|||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"npm-check-updates": "16.14.12"
|
||||
"npm-check-updates": "16.14.14"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Compiling native node packages..."
|
||||
yarn rebuild
|
||||
echo "Packaging static assets..."
|
||||
if [ "${SHOULD_DEPLOY}" = "true" ]; then
|
||||
yarn build --base=https://www.ietf.org/lib/dt/$PKG_VERSION/
|
||||
else
|
||||
yarn build
|
||||
fi
|
||||
yarn legacy:build
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
cp dev/deploy/settings_local_collectstatics.py ietf/settings_local.py
|
||||
|
||||
# Install Python dependencies
|
||||
pip --disable-pip-version-check --no-cache-dir install -r requirements.txt
|
||||
|
||||
# Collect statics
|
||||
ietf/manage.py collectstatic
|
23
helm/.helmignore
Normal file
23
helm/.helmignore
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
23
helm/Chart.yaml
Normal file
23
helm/Chart.yaml
Normal file
|
@ -0,0 +1,23 @@
|
|||
apiVersion: v2
|
||||
name: datatracker
|
||||
description: The day-to-day front-end to the IETF database for people who work on IETF standards.
|
||||
home: https://datatracker.ietf.org
|
||||
sources:
|
||||
- https://github.com/ietf-tools/datatracker
|
||||
maintainers:
|
||||
- name: IETF Tools Team
|
||||
email: tools-discuss@ietf.org
|
||||
url: https://github.com/ietf-tools
|
||||
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.0.0
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.0.0"
|
62
helm/templates/_helpers.tpl
Normal file
62
helm/templates/_helpers.tpl
Normal file
|
@ -0,0 +1,62 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "datatracker.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "datatracker.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "datatracker.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "datatracker.labels" -}}
|
||||
helm.sh/chart: {{ include "datatracker.chart" . }}
|
||||
{{ include "datatracker.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "datatracker.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "datatracker.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "datatracker.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "datatracker.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
66
helm/templates/deployment.yaml
Normal file
66
helm/templates/deployment.yaml
Normal file
|
@ -0,0 +1,66 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "datatracker.fullname" . }}
|
||||
labels:
|
||||
{{- include "datatracker.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "datatracker.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "datatracker.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "datatracker.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}"
|
||||
imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }}
|
||||
env:
|
||||
{{- if .Values.env }}
|
||||
{{- toYaml .Values.env | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.volumeMounts }}
|
||||
volumeMounts:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
{{- toYaml .Values.livenessProbe | nindent 12 }}
|
||||
readinessProbe:
|
||||
{{- toYaml .Values.readinessProbe | nindent 12 }}
|
||||
startupProbe:
|
||||
{{- toYaml .Values.startupProbe | nindent 12 }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.volumes }}
|
||||
volumes:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
32
helm/templates/hpa.yaml
Normal file
32
helm/templates/hpa.yaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "datatracker.fullname" . }}
|
||||
labels:
|
||||
{{- include "datatracker.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "datatracker.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
61
helm/templates/ingress.yaml
Normal file
61
helm/templates/ingress.yaml
Normal file
|
@ -0,0 +1,61 @@
|
|||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "datatracker.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "datatracker.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
19
helm/templates/service.yaml
Normal file
19
helm/templates/service.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{include "datatracker.fullname" .}}
|
||||
labels: {{- include "datatracker.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{- range $key, $value := . }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{.Values.service.type}}
|
||||
ports:
|
||||
- port: {{ default "80" .Values.service.port}}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector: {{- include "datatracker.selectorLabels" . | nindent 4}}
|
12
helm/templates/serviceaccount.yaml
Normal file
12
helm/templates/serviceaccount.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "datatracker.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "datatracker.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
118
helm/values.yaml
Normal file
118
helm/values.yaml
Normal file
|
@ -0,0 +1,118 @@
|
|||
# Default values for datatracker.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: "ghcr.io/ietf-tools/datatracker"
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
# tag: "v1.1.0"
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Automatically mount a ServiceAccount's API credentials?
|
||||
automount: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
|
||||
startupProbe:
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
successThreshold: 1
|
||||
failureThreshold: 60
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
|
||||
podAnnotations: {}
|
||||
podLabels: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: datatracker.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
# Additional volumes on the output Deployment definition.
|
||||
volumes: []
|
||||
# - name: foo
|
||||
# secret:
|
||||
# secretName: mysecret
|
||||
# optional: false
|
||||
|
||||
# Additional volumeMounts on the output Deployment definition.
|
||||
volumeMounts: []
|
||||
# - name: foo
|
||||
# mountPath: "/etc/foo"
|
||||
# readOnly: true
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
|
@ -2,14 +2,75 @@
|
|||
|
||||
# This is not utils.py because Tastypie implicitly consumes ietf.api.utils.
|
||||
# See ietf.api.__init__.py for details.
|
||||
from functools import wraps
|
||||
from typing import Callable, Optional, Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponseForbidden
|
||||
|
||||
|
||||
def is_valid_token(endpoint, token):
|
||||
# This is where we would consider integration with vault
|
||||
# Settings implementation for now.
|
||||
if hasattr(settings, "APP_API_TOKENS"):
|
||||
token_store = settings.APP_API_TOKENS
|
||||
if endpoint in token_store and token in token_store[endpoint]:
|
||||
return True
|
||||
if endpoint in token_store:
|
||||
endpoint_tokens = token_store[endpoint]
|
||||
# Be sure endpoints is a list or tuple so we don't accidentally use substring matching!
|
||||
if not isinstance(endpoint_tokens, (list, tuple)):
|
||||
endpoint_tokens = [endpoint_tokens]
|
||||
if token in endpoint_tokens:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def requires_api_token(func_or_endpoint: Optional[Union[Callable, str]] = None):
|
||||
"""Validate API token before executing the wrapped method
|
||||
|
||||
Usage:
|
||||
* Basic: endpoint defaults to the qualified name of the wrapped method. E.g., in ietf.api.views,
|
||||
|
||||
@requires_api_token
|
||||
def my_view(request):
|
||||
...
|
||||
|
||||
will require a token for "ietf.api.views.my_view"
|
||||
|
||||
* Custom endpoint: specify the endpoint explicitly
|
||||
|
||||
@requires_api_token("ietf.api.views.some_other_thing")
|
||||
def my_view(request):
|
||||
...
|
||||
|
||||
will require a token for "ietf.api.views.some_other_thing"
|
||||
"""
|
||||
|
||||
def decorate(f):
|
||||
if _endpoint is None:
|
||||
fname = getattr(f, "__qualname__", None)
|
||||
if fname is None:
|
||||
raise TypeError(
|
||||
"Cannot automatically decorate function that does not support __qualname__. "
|
||||
"Explicitly set the endpoint."
|
||||
)
|
||||
endpoint = "{}.{}".format(f.__module__, fname)
|
||||
else:
|
||||
endpoint = _endpoint
|
||||
|
||||
@wraps(f)
|
||||
def wrapped(request, *args, **kwargs):
|
||||
authtoken = request.META.get("HTTP_X_API_KEY", None)
|
||||
if authtoken is None or not is_valid_token(endpoint, authtoken):
|
||||
return HttpResponseForbidden()
|
||||
return f(request, *args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
# Magic to allow decorator to be used with or without parentheses
|
||||
if callable(func_or_endpoint):
|
||||
func = func_or_endpoint
|
||||
_endpoint = None
|
||||
return decorate(func)
|
||||
else:
|
||||
_endpoint = func_or_endpoint
|
||||
return decorate
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import datetime
|
||||
import json
|
||||
import html
|
||||
import mock
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
@ -12,7 +13,8 @@ from pathlib import Path
|
|||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.test import Client
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.test import Client, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse as urlreverse
|
||||
from django.utils import timezone
|
||||
|
@ -38,6 +40,8 @@ from ietf.utils.mail import outbox, get_payload_text
|
|||
from ietf.utils.models import DumpInfo
|
||||
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, reload_db_objects
|
||||
|
||||
from .ietf_utils import is_valid_token, requires_api_token
|
||||
|
||||
OMITTED_APPS = (
|
||||
'ietf.secr.meetings',
|
||||
'ietf.secr.proceedings',
|
||||
|
@ -781,6 +785,73 @@ class CustomApiTests(TestCase):
|
|||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
@override_settings(APP_API_TOKENS={"ietf.api.views.email_aliases": ["valid-token"]})
|
||||
@mock.patch("ietf.api.views.DraftAliasGenerator")
|
||||
def test_draft_aliases(self, mock):
|
||||
mock.return_value = (("alias1", ("a1", "a2")), ("alias2", ("a3", "a4")))
|
||||
url = urlreverse("ietf.api.views.draft_aliases")
|
||||
r = self.client.get(url, headers={"X-Api-Key": "valid-token"})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.headers["Content-type"], "application/json")
|
||||
self.assertEqual(
|
||||
json.loads(r.content),
|
||||
{
|
||||
"aliases": [
|
||||
{"alias": "alias1", "domains": ["ietf"], "addresses": ["a1", "a2"]},
|
||||
{"alias": "alias2", "domains": ["ietf"], "addresses": ["a3", "a4"]},
|
||||
]}
|
||||
)
|
||||
# some invalid cases
|
||||
self.assertEqual(
|
||||
self.client.get(url, headers={}).status_code,
|
||||
403,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get(url, headers={"X-Api-Key": "something-else"}).status_code,
|
||||
403,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.post(url, headers={"X-Api-Key": "something-else"}).status_code,
|
||||
403,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.post(url, headers={"X-Api-Key": "valid-token"}).status_code,
|
||||
405,
|
||||
)
|
||||
|
||||
@override_settings(APP_API_TOKENS={"ietf.api.views.email_aliases": ["valid-token"]})
|
||||
@mock.patch("ietf.api.views.GroupAliasGenerator")
|
||||
def test_group_aliases(self, mock):
|
||||
mock.return_value = (("alias1", ("ietf",), ("a1", "a2")), ("alias2", ("ietf", "iab"), ("a3", "a4")))
|
||||
url = urlreverse("ietf.api.views.group_aliases")
|
||||
r = self.client.get(url, headers={"X-Api-Key": "valid-token"})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.headers["Content-type"], "application/json")
|
||||
self.assertEqual(
|
||||
json.loads(r.content),
|
||||
{
|
||||
"aliases": [
|
||||
{"alias": "alias1", "domains": ["ietf"], "addresses": ["a1", "a2"]},
|
||||
{"alias": "alias2", "domains": ["ietf", "iab"], "addresses": ["a3", "a4"]},
|
||||
]}
|
||||
)
|
||||
# some invalid cases
|
||||
self.assertEqual(
|
||||
self.client.get(url, headers={}).status_code,
|
||||
403,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get(url, headers={"X-Api-Key": "something-else"}).status_code,
|
||||
403,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.post(url, headers={"X-Api-Key": "something-else"}).status_code,
|
||||
403,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.post(url, headers={"X-Api-Key": "valid-token"}).status_code,
|
||||
405,
|
||||
)
|
||||
|
||||
|
||||
class DirectAuthApiTests(TestCase):
|
||||
|
@ -1133,3 +1204,85 @@ class RfcdiffSupportTests(TestCase):
|
|||
url = urlreverse(self.target_view, kwargs={'name': name})
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
|
||||
class TokenTests(TestCase):
|
||||
@override_settings(APP_API_TOKENS={"known.endpoint": ["token in a list"], "oops": "token as a str"})
|
||||
def test_is_valid_token(self):
|
||||
# various invalid cases
|
||||
self.assertFalse(is_valid_token("unknown.endpoint", "token in a list"))
|
||||
self.assertFalse(is_valid_token("known.endpoint", "token"))
|
||||
self.assertFalse(is_valid_token("known.endpoint", "token as a str"))
|
||||
self.assertFalse(is_valid_token("oops", "token"))
|
||||
self.assertFalse(is_valid_token("oops", "token in a list"))
|
||||
# the only valid cases
|
||||
self.assertTrue(is_valid_token("known.endpoint", "token in a list"))
|
||||
self.assertTrue(is_valid_token("oops", "token as a str"))
|
||||
|
||||
@mock.patch("ietf.api.ietf_utils.is_valid_token")
|
||||
def test_requires_api_token(self, mock_is_valid_token):
|
||||
called = False
|
||||
|
||||
@requires_api_token
|
||||
def fn_to_wrap(request, *args, **kwargs):
|
||||
nonlocal called
|
||||
called = True
|
||||
return request, args, kwargs
|
||||
|
||||
req_factory = RequestFactory()
|
||||
arg = object()
|
||||
kwarg = object()
|
||||
|
||||
# No X-Api-Key header
|
||||
mock_is_valid_token.return_value = False
|
||||
val = fn_to_wrap(
|
||||
req_factory.get("/some/url", headers={}),
|
||||
arg,
|
||||
kwarg=kwarg,
|
||||
)
|
||||
self.assertTrue(isinstance(val, HttpResponseForbidden))
|
||||
self.assertFalse(mock_is_valid_token.called)
|
||||
self.assertFalse(called)
|
||||
|
||||
# Bad X-Api-Key header (not resetting the mock, it was not used yet)
|
||||
val = fn_to_wrap(
|
||||
req_factory.get("/some/url", headers={"X-Api-Key": "some-value"}),
|
||||
arg,
|
||||
kwarg=kwarg,
|
||||
)
|
||||
self.assertTrue(isinstance(val, HttpResponseForbidden))
|
||||
self.assertTrue(mock_is_valid_token.called)
|
||||
self.assertEqual(
|
||||
mock_is_valid_token.call_args[0],
|
||||
(fn_to_wrap.__module__ + "." + fn_to_wrap.__qualname__, "some-value"),
|
||||
)
|
||||
self.assertFalse(called)
|
||||
|
||||
# Valid header
|
||||
mock_is_valid_token.reset_mock()
|
||||
mock_is_valid_token.return_value = True
|
||||
request = req_factory.get("/some/url", headers={"X-Api-Key": "some-value"})
|
||||
# Bad X-Api-Key header (not resetting the mock, it was not used yet)
|
||||
val = fn_to_wrap(
|
||||
request,
|
||||
arg,
|
||||
kwarg=kwarg,
|
||||
)
|
||||
self.assertEqual(val, (request, (arg,), {"kwarg": kwarg}))
|
||||
self.assertTrue(mock_is_valid_token.called)
|
||||
self.assertEqual(
|
||||
mock_is_valid_token.call_args[0],
|
||||
(fn_to_wrap.__module__ + "." + fn_to_wrap.__qualname__, "some-value"),
|
||||
)
|
||||
self.assertTrue(called)
|
||||
|
||||
# Test the endpoint setting
|
||||
@requires_api_token("endpoint")
|
||||
def another_fn_to_wrap(request):
|
||||
return "yep"
|
||||
|
||||
val = another_fn_to_wrap(request)
|
||||
self.assertEqual(
|
||||
mock_is_valid_token.call_args[0],
|
||||
("endpoint", "some-value"),
|
||||
)
|
||||
|
|
|
@ -22,8 +22,12 @@ urlpatterns = [
|
|||
url(r'^v2/person/person', api_views.ApiV2PersonExportView.as_view()),
|
||||
#
|
||||
# --- Custom API endpoints, sorted alphabetically ---
|
||||
# Email alias information for drafts
|
||||
url(r'^doc/draft-aliases/$', api_views.draft_aliases),
|
||||
# GPRD: export of personal information for the logged-in person
|
||||
url(r'^export/personal-information/$', api_views.PersonalInformationExportView.as_view()),
|
||||
# Email alias information for groups
|
||||
url(r'^group/group-aliases/$', api_views.group_aliases),
|
||||
# Let IESG members set positions programmatically
|
||||
url(r'^iesg/position', views_ballot.api_set_position),
|
||||
# Let Meetecho set session video URLs
|
||||
|
|
|
@ -2,42 +2,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import pytz
|
||||
import re
|
||||
|
||||
from jwcrypto.jwk import JWK
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.http import HttpResponse, Http404, JsonResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.views.generic.detail import DetailView
|
||||
|
||||
from jwcrypto.jwk import JWK
|
||||
from tastypie.exceptions import BadRequest
|
||||
from tastypie.utils.mime import determine_format, build_content_type
|
||||
from tastypie.utils import is_valid_jsonp_callback_value
|
||||
from tastypie.serializers import Serializer
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
from tastypie.utils import is_valid_jsonp_callback_value
|
||||
from tastypie.utils.mime import determine_format, build_content_type
|
||||
|
||||
import ietf
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.api import _api_list
|
||||
from ietf.api.ietf_utils import is_valid_token, requires_api_token
|
||||
from ietf.api.serializer import JsonExportMixin
|
||||
from ietf.api.ietf_utils import is_valid_token
|
||||
from ietf.doc.utils import fuzzy_find_documents
|
||||
from ietf.ietfauth.views import send_account_creation_email
|
||||
from ietf.doc.utils import DraftAliasGenerator, fuzzy_find_documents
|
||||
from ietf.group.utils import GroupAliasGenerator
|
||||
from ietf.ietfauth.utils import role_required
|
||||
from ietf.ietfauth.views import send_account_creation_email
|
||||
from ietf.meeting.models import Meeting
|
||||
from ietf.nomcom.models import Volunteer, NomCom
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.stats.models import MeetingRegistration
|
||||
from ietf.utils import log
|
||||
from ietf.utils.decorators import require_api_key
|
||||
|
@ -453,3 +450,41 @@ def directauth(request):
|
|||
|
||||
else:
|
||||
return HttpResponse(status=405)
|
||||
|
||||
|
||||
@requires_api_token("ietf.api.views.email_aliases")
|
||||
@csrf_exempt
|
||||
def draft_aliases(request):
|
||||
if request.method == "GET":
|
||||
return JsonResponse(
|
||||
{
|
||||
"aliases": [
|
||||
{
|
||||
"alias": alias,
|
||||
"domains": ["ietf"],
|
||||
"addresses": address_list,
|
||||
}
|
||||
for alias, address_list in DraftAliasGenerator()
|
||||
]
|
||||
}
|
||||
)
|
||||
return HttpResponse(status=405)
|
||||
|
||||
|
||||
@requires_api_token("ietf.api.views.email_aliases")
|
||||
@csrf_exempt
|
||||
def group_aliases(request):
|
||||
if request.method == "GET":
|
||||
return JsonResponse(
|
||||
{
|
||||
"aliases": [
|
||||
{
|
||||
"alias": alias,
|
||||
"domains": domains,
|
||||
"addresses": address_list,
|
||||
}
|
||||
for alias, domains, address_list in GroupAliasGenerator()
|
||||
]
|
||||
}
|
||||
)
|
||||
return HttpResponse(status=405)
|
||||
|
|
99
ietf/bin/aliases-from-json.py
Normal file
99
ietf/bin/aliases-from-json.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
#
|
||||
# Uses only Python standard lib
|
||||
#
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import shutil
|
||||
import stat
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
# Default options
|
||||
POSTCONFIRM_PATH = "/a/postconfirm/wrapper"
|
||||
VDOMAIN = "virtual.ietf.org"
|
||||
|
||||
# Map from domain label to dns domain
|
||||
ADOMAINS = {
|
||||
"ietf": "ietf.org",
|
||||
"irtf": "irtf.org",
|
||||
"iab": "iab.org",
|
||||
}
|
||||
|
||||
|
||||
def generate_files(records, adest, vdest, postconfirm, vdomain):
|
||||
"""Generate files from an iterable of records
|
||||
|
||||
If adest or vdest exists as a file, it will be overwritten. If it is a directory, files
|
||||
with the default names (draft-aliases and draft-virtual) will be created, but existing
|
||||
files _will not_ be overwritten!
|
||||
"""
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
tmppath = Path(tmpdir)
|
||||
apath = tmppath / "aliases"
|
||||
vpath = tmppath / "virtual"
|
||||
|
||||
with apath.open("w") as afile, vpath.open("w") as vfile:
|
||||
date = datetime.datetime.now(datetime.timezone.utc)
|
||||
signature = f"# Generated by {Path(__file__).absolute()} at {date}\n"
|
||||
afile.write(signature)
|
||||
vfile.write(signature)
|
||||
vfile.write(f"{vdomain} anything\n")
|
||||
|
||||
for item in records:
|
||||
alias = item["alias"]
|
||||
domains = item["domains"]
|
||||
address_list = item["addresses"]
|
||||
filtername = f"xfilter-{alias}"
|
||||
afile.write(f'{filtername + ":":64s} "|{postconfirm} filter expand-{alias} {vdomain}"\n')
|
||||
for dom in domains:
|
||||
vfile.write(f"{f'{alias}@{ADOMAINS[dom]}':64s} {filtername}\n")
|
||||
vfile.write(f"{f'expand-{alias}@{vdomain}':64s} {', '.join(sorted(address_list))}\n")
|
||||
|
||||
perms = stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
apath.chmod(perms)
|
||||
vpath.chmod(perms)
|
||||
shutil.move(apath, adest)
|
||||
shutil.move(vpath, vdest)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert a JSON stream of draft alias definitions into alias / virtual alias files."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prefix",
|
||||
required=True,
|
||||
help="Prefix for output files. Files will be named <prefix>-aliases and <prefix>-virtual."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default="./",
|
||||
type=Path,
|
||||
help="Destination for output files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--postconfirm",
|
||||
default=POSTCONFIRM_PATH,
|
||||
help=f"Full path to postconfirm executable (defaults to {POSTCONFIRM_PATH}",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vdomain",
|
||||
default=VDOMAIN,
|
||||
help=f"Virtual domain (defaults to {VDOMAIN}_",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if not args.output_dir.is_dir():
|
||||
sys.stderr.write("Error: output-dir must be a directory")
|
||||
data = json.load(sys.stdin)
|
||||
generate_files(
|
||||
data["aliases"],
|
||||
adest=args.output_dir / f"{args.prefix}-aliases",
|
||||
vdest=args.output_dir / f"{args.prefix}-virtual",
|
||||
postconfirm=args.postconfirm,
|
||||
vdomain=args.vdomain,
|
||||
)
|
|
@ -1,50 +0,0 @@
|
|||
#!/usr/bin/python2.7
|
||||
# Copyright The IETF Trust 2022, All Rights Reserved
|
||||
# Note the shebang. This specifically targets deployment on IETFA and intends to use its system python2.7.
|
||||
|
||||
# This is an adaptor to pull information out of Mailman2 using its python libraries (which are only available for python2).
|
||||
# It is NOT django code, and does not have access to django.conf.settings.
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
def main():
|
||||
|
||||
sys.path.append('/usr/lib/mailman')
|
||||
|
||||
have_mailman = False
|
||||
try:
|
||||
from Mailman import Utils
|
||||
from Mailman import MailList
|
||||
from Mailman import MemberAdaptor
|
||||
have_mailman = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
if not have_mailman:
|
||||
sys.stderr.write("Could not import mailman modules -- skipping import of mailman list info")
|
||||
sys.exit()
|
||||
|
||||
names = list(Utils.list_names())
|
||||
|
||||
# need to emit dict of names, each name has an mlist, and each mlist has description, advertised, and members (calculated as below)
|
||||
result = defaultdict(dict)
|
||||
for name in names:
|
||||
mlist = MailList.MailList(name, lock=False)
|
||||
result[name] = dict()
|
||||
result[name]['internal_name'] = mlist.internal_name()
|
||||
result[name]['real_name'] = mlist.real_name
|
||||
result[name]['description'] = mlist.description # Not attempting to change encoding
|
||||
result[name]['advertised'] = mlist.advertised
|
||||
result[name]['members'] = list()
|
||||
if mlist.advertised:
|
||||
members = mlist.getRegularMemberKeys() + mlist.getDigestMemberKeys()
|
||||
members = set([ m for m in members if mlist.getDeliveryStatus(m) == MemberAdaptor.ENABLED ])
|
||||
result[name]['members'] = list(members)
|
||||
json.dump(result, sys.stdout)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -280,6 +280,19 @@ class DocumentInfo(models.Model):
|
|||
info = dict(doc=self)
|
||||
|
||||
href = format.format(**info)
|
||||
|
||||
# For slides that are not meeting-related, we need to know the file extension.
|
||||
# Assume we have access to the same files as settings.DOC_HREFS["slides"] and
|
||||
# see what extension is available
|
||||
if self.type_id == "slides" and not self.meeting_related() and not href.endswith("/"):
|
||||
filepath = Path(self.get_file_path()) / self.get_base_name() # start with this
|
||||
if not filepath.exists():
|
||||
# Look for other extensions - grab the first one, sorted for stability
|
||||
for existing in sorted(filepath.parent.glob(f"{filepath.stem}.*")):
|
||||
filepath = filepath.with_suffix(existing.suffix)
|
||||
break
|
||||
href += filepath.suffix # tack on the extension
|
||||
|
||||
if href.startswith('/'):
|
||||
href = settings.IDTRACKER_BASE_URL + href
|
||||
self._cached_href = href
|
||||
|
|
56
ietf/doc/tasks.py
Normal file
56
ietf/doc/tasks.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
#
|
||||
# Celery task definitions
|
||||
#
|
||||
import datetime
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from ietf.utils import log
|
||||
from ietf.utils.timezone import datetime_today
|
||||
|
||||
from .expire import (
|
||||
in_draft_expire_freeze,
|
||||
get_expired_drafts,
|
||||
expirable_drafts,
|
||||
send_expire_notice_for_draft,
|
||||
expire_draft,
|
||||
clean_up_draft_files,
|
||||
get_soon_to_expire_drafts,
|
||||
send_expire_warning_for_draft,
|
||||
)
|
||||
from .models import Document
|
||||
|
||||
|
||||
@shared_task
|
||||
def expire_ids_task():
|
||||
try:
|
||||
if not in_draft_expire_freeze():
|
||||
log.log("Expiring drafts ...")
|
||||
for doc in get_expired_drafts():
|
||||
# verify expirability -- it might have changed after get_expired_drafts() was run
|
||||
# (this whole loop took about 2 minutes on 04 Jan 2018)
|
||||
# N.B., re-running expirable_drafts() repeatedly is fairly expensive. Where possible,
|
||||
# it's much faster to run it once on a superset query of the objects you are going
|
||||
# to test and keep its results. That's not desirable here because it would defeat
|
||||
# the purpose of double-checking that a document is still expirable when it is actually
|
||||
# being marked as expired.
|
||||
if expirable_drafts(
|
||||
Document.objects.filter(pk=doc.pk)
|
||||
).exists() and doc.expires < datetime_today() + datetime.timedelta(1):
|
||||
send_expire_notice_for_draft(doc)
|
||||
expire_draft(doc)
|
||||
log.log(f" Expired draft {doc.name}-{doc.rev}")
|
||||
|
||||
log.log("Cleaning up draft files")
|
||||
clean_up_draft_files()
|
||||
except Exception as e:
|
||||
log.log("Exception in expire-ids: %s" % e)
|
||||
raise
|
||||
|
||||
|
||||
@shared_task
|
||||
def notify_expirations_task(notify_days=14):
|
||||
for doc in get_soon_to_expire_drafts(notify_days):
|
||||
send_expire_warning_for_draft(doc)
|
|
@ -45,7 +45,7 @@ from ietf.doc.factories import ( DocumentFactory, DocEventFactory, CharterFactor
|
|||
StatusChangeFactory, DocExtResourceFactory, RgDraftFactory, BcpFactory)
|
||||
from ietf.doc.forms import NotifyForm
|
||||
from ietf.doc.fields import SearchableDocumentsField
|
||||
from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_name
|
||||
from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_name, DraftAliasGenerator
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.group.factories import GroupFactory, RoleFactory
|
||||
from ietf.ipr.factories import HolderIprDisclosureFactory
|
||||
|
@ -2291,6 +2291,7 @@ class GenerateDraftAliasesTests(TestCase):
|
|||
"xfilter-" + doc3.name + ".ad",
|
||||
"xfilter-" + doc3.name + ".authors",
|
||||
"xfilter-" + doc3.name + ".chairs",
|
||||
"xfilter-" + doc3.name + ".all",
|
||||
"xfilter-" + doc5.name,
|
||||
"xfilter-" + doc5.name + ".authors",
|
||||
"xfilter-" + doc5.name + ".all",
|
||||
|
@ -2307,6 +2308,148 @@ class GenerateDraftAliasesTests(TestCase):
|
|||
]:
|
||||
self.assertNotIn(x, vcontent)
|
||||
|
||||
@override_settings(TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org")
|
||||
def test_generator_class(self):
|
||||
"""The DraftAliasGenerator should generate the same lists as the old mgmt cmd"""
|
||||
a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(RPC_TZINFO)
|
||||
a_month_ago = a_month_ago.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
ad = RoleFactory(
|
||||
name_id="ad", group__type_id="area", group__state_id="active"
|
||||
).person
|
||||
shepherd = PersonFactory()
|
||||
author1 = PersonFactory()
|
||||
author2 = PersonFactory()
|
||||
author3 = PersonFactory()
|
||||
author4 = PersonFactory()
|
||||
author5 = PersonFactory()
|
||||
author6 = PersonFactory()
|
||||
mars = GroupFactory(type_id="wg", acronym="mars")
|
||||
marschairman = PersonFactory(user__username="marschairman")
|
||||
mars.role_set.create(
|
||||
name_id="chair", person=marschairman, email=marschairman.email()
|
||||
)
|
||||
doc1 = IndividualDraftFactory(authors=[author1], shepherd=shepherd.email(), ad=ad)
|
||||
doc2 = WgDraftFactory(
|
||||
name="draft-ietf-mars-test", group__acronym="mars", authors=[author2], ad=ad
|
||||
)
|
||||
doc2.notify = f"{doc2.name}.ad@draft.example.org"
|
||||
doc2.save()
|
||||
doc3 = WgDraftFactory.create(
|
||||
name="draft-ietf-mars-finished",
|
||||
group__acronym="mars",
|
||||
authors=[author3],
|
||||
ad=ad,
|
||||
std_level_id="ps",
|
||||
states=[("draft", "rfc"), ("draft-iesg", "pub")],
|
||||
time=a_month_ago,
|
||||
)
|
||||
rfc3 = WgRfcFactory()
|
||||
DocEventFactory.create(doc=rfc3, type="published_rfc", time=a_month_ago)
|
||||
doc3.relateddocument_set.create(relationship_id="became_rfc", target=rfc3)
|
||||
doc4 = WgDraftFactory.create(
|
||||
authors=[author4, author5],
|
||||
ad=ad,
|
||||
std_level_id="ps",
|
||||
states=[("draft", "rfc"), ("draft-iesg", "pub")],
|
||||
time=datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)),
|
||||
)
|
||||
rfc4 = WgRfcFactory()
|
||||
DocEventFactory.create(
|
||||
doc=rfc4,
|
||||
type="published_rfc",
|
||||
time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO),
|
||||
)
|
||||
doc4.relateddocument_set.create(relationship_id="became_rfc", target=rfc4)
|
||||
doc5 = IndividualDraftFactory(authors=[author6])
|
||||
|
||||
output = [(alias, alist) for alias, alist in DraftAliasGenerator()]
|
||||
alias_dict = dict(output)
|
||||
self.assertEqual(len(alias_dict), len(output)) # no duplicate aliases
|
||||
expected_dict = {
|
||||
doc1.name: [author1.email_address()],
|
||||
doc1.name + ".ad": [ad.email_address()],
|
||||
doc1.name + ".authors": [author1.email_address()],
|
||||
doc1.name + ".shepherd": [shepherd.email_address()],
|
||||
doc1.name
|
||||
+ ".all": [
|
||||
author1.email_address(),
|
||||
ad.email_address(),
|
||||
shepherd.email_address(),
|
||||
],
|
||||
doc2.name: [author2.email_address()],
|
||||
doc2.name + ".ad": [ad.email_address()],
|
||||
doc2.name + ".authors": [author2.email_address()],
|
||||
doc2.name + ".chairs": [marschairman.email_address()],
|
||||
doc2.name + ".notify": [ad.email_address()],
|
||||
doc2.name
|
||||
+ ".all": [
|
||||
author2.email_address(),
|
||||
ad.email_address(),
|
||||
marschairman.email_address(),
|
||||
],
|
||||
doc3.name: [author3.email_address()],
|
||||
doc3.name + ".ad": [ad.email_address()],
|
||||
doc3.name + ".authors": [author3.email_address()],
|
||||
doc3.name + ".chairs": [marschairman.email_address()],
|
||||
doc3.name
|
||||
+ ".all": [
|
||||
author3.email_address(),
|
||||
ad.email_address(),
|
||||
marschairman.email_address(),
|
||||
],
|
||||
doc5.name: [author6.email_address()],
|
||||
doc5.name + ".authors": [author6.email_address()],
|
||||
doc5.name + ".all": [author6.email_address()],
|
||||
}
|
||||
# Sort lists for comparison
|
||||
self.assertEqual(
|
||||
{k: sorted(v) for k, v in alias_dict.items()},
|
||||
{k: sorted(v) for k, v in expected_dict.items()},
|
||||
)
|
||||
|
||||
@override_settings(TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org")
|
||||
def test_get_draft_notify_emails(self):
|
||||
ad = PersonFactory()
|
||||
shepherd = PersonFactory()
|
||||
author = PersonFactory()
|
||||
doc = DocumentFactory(authors=[author], shepherd=shepherd.email(), ad=ad)
|
||||
generator = DraftAliasGenerator()
|
||||
|
||||
doc.notify = f"{doc.name}@draft.example.org"
|
||||
doc.save()
|
||||
self.assertCountEqual(generator.get_draft_notify_emails(doc), [author.email_address()])
|
||||
|
||||
doc.notify = f"{doc.name}.ad@draft.example.org"
|
||||
doc.save()
|
||||
self.assertCountEqual(generator.get_draft_notify_emails(doc), [ad.email_address()])
|
||||
|
||||
doc.notify = f"{doc.name}.shepherd@draft.example.org"
|
||||
doc.save()
|
||||
self.assertCountEqual(generator.get_draft_notify_emails(doc), [shepherd.email_address()])
|
||||
|
||||
doc.notify = f"{doc.name}.all@draft.example.org"
|
||||
doc.save()
|
||||
self.assertCountEqual(
|
||||
generator.get_draft_notify_emails(doc),
|
||||
[ad.email_address(), author.email_address(), shepherd.email_address()]
|
||||
)
|
||||
|
||||
doc.notify = f"{doc.name}.notify@draft.example.org"
|
||||
doc.save()
|
||||
self.assertCountEqual(generator.get_draft_notify_emails(doc), [])
|
||||
|
||||
doc.notify = f"{doc.name}.ad@somewhere.example.com"
|
||||
doc.save()
|
||||
self.assertCountEqual(generator.get_draft_notify_emails(doc), [f"{doc.name}.ad@somewhere.example.com"])
|
||||
|
||||
doc.notify = f"somebody@example.com, nobody@example.com, {doc.name}.ad@tools.example.org"
|
||||
doc.save()
|
||||
self.assertCountEqual(
|
||||
generator.get_draft_notify_emails(doc),
|
||||
["somebody@example.com", "nobody@example.com", ad.email_address()]
|
||||
)
|
||||
|
||||
|
||||
class EmailAliasesTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
63
ietf/doc/tests_tasks.py
Normal file
63
ietf/doc/tests_tasks.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
import mock
|
||||
|
||||
from ietf.utils.test_utils import TestCase
|
||||
from ietf.utils.timezone import datetime_today
|
||||
|
||||
from .factories import DocumentFactory
|
||||
from .models import Document
|
||||
from .tasks import expire_ids_task, notify_expirations_task
|
||||
|
||||
|
||||
class TaskTests(TestCase):
|
||||
|
||||
@mock.patch("ietf.doc.tasks.in_draft_expire_freeze")
|
||||
@mock.patch("ietf.doc.tasks.get_expired_drafts")
|
||||
@mock.patch("ietf.doc.tasks.expirable_drafts")
|
||||
@mock.patch("ietf.doc.tasks.send_expire_notice_for_draft")
|
||||
@mock.patch("ietf.doc.tasks.expire_draft")
|
||||
@mock.patch("ietf.doc.tasks.clean_up_draft_files")
|
||||
def test_expire_ids_task(
|
||||
self,
|
||||
clean_up_draft_files_mock,
|
||||
expire_draft_mock,
|
||||
send_expire_notice_for_draft_mock,
|
||||
expirable_drafts_mock,
|
||||
get_expired_drafts_mock,
|
||||
in_draft_expire_freeze_mock,
|
||||
):
|
||||
# set up mocks
|
||||
in_draft_expire_freeze_mock.return_value = False
|
||||
doc, other_doc = DocumentFactory.create_batch(2)
|
||||
doc.expires = datetime_today()
|
||||
get_expired_drafts_mock.return_value = [doc, other_doc]
|
||||
expirable_drafts_mock.side_effect = [
|
||||
Document.objects.filter(pk=doc.pk),
|
||||
Document.objects.filter(pk=other_doc.pk),
|
||||
]
|
||||
|
||||
# call task
|
||||
expire_ids_task()
|
||||
|
||||
# check results
|
||||
self.assertTrue(in_draft_expire_freeze_mock.called)
|
||||
self.assertEqual(expirable_drafts_mock.call_count, 2)
|
||||
self.assertEqual(send_expire_notice_for_draft_mock.call_count, 1)
|
||||
self.assertEqual(send_expire_notice_for_draft_mock.call_args[0], (doc,))
|
||||
self.assertEqual(expire_draft_mock.call_count, 1)
|
||||
self.assertEqual(expire_draft_mock.call_args[0], (doc,))
|
||||
self.assertTrue(clean_up_draft_files_mock.called)
|
||||
|
||||
# test that an exception is raised
|
||||
in_draft_expire_freeze_mock.side_effect = RuntimeError
|
||||
with self.assertRaises(RuntimeError):(
|
||||
expire_ids_task())
|
||||
|
||||
@mock.patch("ietf.doc.tasks.send_expire_warning_for_draft")
|
||||
@mock.patch("ietf.doc.tasks.get_soon_to_expire_drafts")
|
||||
def test_notify_expirations_task(self, get_drafts_mock, send_warning_mock):
|
||||
# Set up mocks
|
||||
get_drafts_mock.return_value = ["sentinel"]
|
||||
notify_expirations_task()
|
||||
self.assertEqual(send_warning_mock.call_count, 1)
|
||||
self.assertEqual(send_warning_mock.call_args[0], ("sentinel",))
|
|
@ -13,7 +13,7 @@ import textwrap
|
|||
|
||||
from collections import defaultdict, namedtuple, Counter
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
from typing import Iterator, Union
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -41,7 +41,7 @@ from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, is_indivi
|
|||
from ietf.person.models import Person
|
||||
from ietf.review.models import ReviewWish
|
||||
from ietf.utils import draft, log
|
||||
from ietf.utils.mail import send_mail
|
||||
from ietf.utils.mail import parseaddr, send_mail
|
||||
from ietf.mailtrigger.utils import gather_address_lists
|
||||
from ietf.utils.timezone import date_today, datetime_from_date, datetime_today, DEADLINE_TZINFO
|
||||
from ietf.utils.xmldraft import XMLDraft
|
||||
|
@ -1258,3 +1258,125 @@ def bibxml_for_draft(doc, rev=None):
|
|||
|
||||
return render_to_string('doc/bibxml.xml', {'name':name, 'doc':doc, 'doc_bibtype':'I-D', 'settings':settings})
|
||||
|
||||
|
||||
class DraftAliasGenerator:
|
||||
days = 2 * 365
|
||||
|
||||
def get_draft_ad_emails(self, doc):
|
||||
"""Get AD email addresses for the given draft, if any."""
|
||||
from ietf.group.utils import get_group_ad_emails # avoid circular import
|
||||
ad_emails = set()
|
||||
# If working group document, return current WG ADs
|
||||
if doc.group and doc.group.acronym != "none":
|
||||
ad_emails.update(get_group_ad_emails(doc.group))
|
||||
# Document may have an explicit AD set
|
||||
if doc.ad:
|
||||
ad_emails.add(doc.ad.email_address())
|
||||
return ad_emails
|
||||
|
||||
def get_draft_chair_emails(self, doc):
|
||||
"""Get chair email addresses for the given draft, if any."""
|
||||
from ietf.group.utils import get_group_role_emails # avoid circular import
|
||||
chair_emails = set()
|
||||
if doc.group:
|
||||
chair_emails.update(get_group_role_emails(doc.group, ["chair", "secr"]))
|
||||
return chair_emails
|
||||
|
||||
def get_draft_shepherd_email(self, doc):
|
||||
"""Get shepherd email addresses for the given draft, if any."""
|
||||
shepherd_email = set()
|
||||
if doc.shepherd:
|
||||
shepherd_email.add(doc.shepherd.email_address())
|
||||
return shepherd_email
|
||||
|
||||
def get_draft_authors_emails(self, doc):
|
||||
"""Get list of authors for the given draft."""
|
||||
author_emails = set()
|
||||
for author in doc.documentauthor_set.all():
|
||||
if author.email and author.email.email_address():
|
||||
author_emails.add(author.email.email_address())
|
||||
return author_emails
|
||||
|
||||
def get_draft_notify_emails(self, doc):
|
||||
"""Get list of email addresses to notify for the given draft."""
|
||||
ad_email_alias_regex = r"^%s.ad@(%s|%s)$" % (doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER)
|
||||
all_email_alias_regex = r"^%s.all@(%s|%s)$" % (doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER)
|
||||
author_email_alias_regex = r"^%s@(%s|%s)$" % (doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER)
|
||||
notify_email_alias_regex = r"^%s.notify@(%s|%s)$" % (
|
||||
doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER)
|
||||
shepherd_email_alias_regex = r"^%s.shepherd@(%s|%s)$" % (
|
||||
doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER)
|
||||
notify_emails = set()
|
||||
if doc.notify:
|
||||
for e in doc.notify.split(','):
|
||||
e = e.strip()
|
||||
if re.search(ad_email_alias_regex, e):
|
||||
notify_emails.update(self.get_draft_ad_emails(doc))
|
||||
elif re.search(author_email_alias_regex, e):
|
||||
notify_emails.update(self.get_draft_authors_emails(doc))
|
||||
elif re.search(shepherd_email_alias_regex, e):
|
||||
notify_emails.update(self.get_draft_shepherd_email(doc))
|
||||
elif re.search(all_email_alias_regex, e):
|
||||
notify_emails.update(self.get_draft_ad_emails(doc))
|
||||
notify_emails.update(self.get_draft_authors_emails(doc))
|
||||
notify_emails.update(self.get_draft_shepherd_email(doc))
|
||||
elif re.search(notify_email_alias_regex, e):
|
||||
pass
|
||||
else:
|
||||
(name, email) = parseaddr(e)
|
||||
notify_emails.add(email)
|
||||
return notify_emails
|
||||
|
||||
def __iter__(self) -> Iterator[tuple[str, list[str]]]:
|
||||
# Internet-Drafts with active status or expired within self.days
|
||||
show_since = timezone.now() - datetime.timedelta(days=self.days)
|
||||
drafts = Document.objects.filter(type_id="draft")
|
||||
active_drafts = drafts.filter(states__slug='active')
|
||||
inactive_recent_drafts = drafts.exclude(states__slug='active').filter(expires__gte=show_since)
|
||||
interesting_drafts = active_drafts | inactive_recent_drafts
|
||||
|
||||
for this_draft in interesting_drafts.distinct().iterator():
|
||||
# Omit drafts that became RFCs, unless they were published in the last DEFAULT_YEARS
|
||||
if this_draft.get_state_slug() == "rfc":
|
||||
rfc = this_draft.became_rfc()
|
||||
log.assertion("rfc is not None")
|
||||
if rfc.latest_event(type='published_rfc').time < show_since:
|
||||
continue
|
||||
|
||||
alias = this_draft.name
|
||||
all = set()
|
||||
|
||||
# no suffix and .authors are the same list
|
||||
emails = self.get_draft_authors_emails(this_draft)
|
||||
all.update(emails)
|
||||
if emails:
|
||||
yield alias, list(emails)
|
||||
yield alias + ".authors", list(emails)
|
||||
|
||||
# .chairs = group chairs
|
||||
emails = self.get_draft_chair_emails(this_draft)
|
||||
if emails:
|
||||
all.update(emails)
|
||||
yield alias + ".chairs", list(emails)
|
||||
|
||||
# .ad = sponsoring AD / WG AD (WG document)
|
||||
emails = self.get_draft_ad_emails(this_draft)
|
||||
if emails:
|
||||
all.update(emails)
|
||||
yield alias + ".ad", list(emails)
|
||||
|
||||
# .notify = notify email list from the Document
|
||||
emails = self.get_draft_notify_emails(this_draft)
|
||||
if emails:
|
||||
all.update(emails)
|
||||
yield alias + ".notify", list(emails)
|
||||
|
||||
# .shepherd = shepherd email from the Document
|
||||
emails = self.get_draft_shepherd_email(this_draft)
|
||||
if emails:
|
||||
all.update(emails)
|
||||
yield alias + ".shepherd", list(emails)
|
||||
|
||||
# .all = everything from above
|
||||
if all:
|
||||
yield alias + ".all", list(all)
|
||||
|
|
|
@ -20,7 +20,7 @@ import debug # pyflakes:ignore
|
|||
from ietf.doc.factories import DocumentFactory, WgDraftFactory, EditorialDraftFactory
|
||||
from ietf.doc.models import DocEvent, RelatedDocument, Document
|
||||
from ietf.group.models import Role, Group
|
||||
from ietf.group.utils import get_group_role_emails, get_child_group_role_emails, get_group_ad_emails
|
||||
from ietf.group.utils import get_group_role_emails, get_child_group_role_emails, get_group_ad_emails, GroupAliasGenerator
|
||||
from ietf.group.factories import GroupFactory, RoleFactory
|
||||
from ietf.person.factories import PersonFactory, EmailFactory
|
||||
from ietf.person.models import Person
|
||||
|
@ -163,7 +163,7 @@ class GenerateGroupAliasesTests(TestCase):
|
|||
recent = GroupFactory(type_id='wg', acronym='recent', parent=area, state_id='conclude', time=a_month_ago)
|
||||
recentchair = PersonFactory(user__username='recentchair')
|
||||
recent.role_set.create(name_id='chair', person=recentchair, email=recentchair.email())
|
||||
wayold = GroupFactory(type_id='wg', acronym='recent', parent=area, state_id='conclude', time=a_decade_ago)
|
||||
wayold = GroupFactory(type_id='wg', acronym='wayold', parent=area, state_id='conclude', time=a_decade_ago)
|
||||
wayoldchair = PersonFactory(user__username='wayoldchair')
|
||||
wayold.role_set.create(name_id='chair', person=wayoldchair, email=wayoldchair.email())
|
||||
role2 = RoleFactory(name_id='ad', group__type_id='area', group__acronym='done', group__state_id='conclude')
|
||||
|
@ -220,7 +220,7 @@ class GenerateGroupAliasesTests(TestCase):
|
|||
testrgchair.email_address(),
|
||||
testragchair.email_address(),
|
||||
]]))
|
||||
self.assertFalse(all([x in vcontent for x in [
|
||||
self.assertFalse(any([x in vcontent for x in [
|
||||
done_ad.email_address(),
|
||||
wayoldchair.email_address(),
|
||||
individual.email_address(),
|
||||
|
@ -248,6 +248,64 @@ class GenerateGroupAliasesTests(TestCase):
|
|||
'xfilter-' + wayold.acronym + '-chairs',
|
||||
]]))
|
||||
|
||||
def test_generator_class(self):
|
||||
"""The GroupAliasGenerator should generate the same lists as the old mgmt cmd"""
|
||||
# clean out test fixture group roles we don't need for this test
|
||||
Role.objects.filter(
|
||||
group__acronym__in=["farfut", "iab", "ietf", "irtf", "ise", "ops", "rsab", "rsoc", "sops"]
|
||||
).delete()
|
||||
|
||||
a_month_ago = timezone.now() - datetime.timedelta(30)
|
||||
a_decade_ago = timezone.now() - datetime.timedelta(3650)
|
||||
role1 = RoleFactory(name_id='ad', group__type_id='area', group__acronym='myth', group__state_id='active')
|
||||
area = role1.group
|
||||
ad = role1.person
|
||||
mars = GroupFactory(type_id='wg', acronym='mars', parent=area)
|
||||
marschair = PersonFactory(user__username='marschair')
|
||||
mars.role_set.create(name_id='chair', person=marschair, email=marschair.email())
|
||||
marssecr = PersonFactory(user__username='marssecr')
|
||||
mars.role_set.create(name_id='secr', person=marssecr, email=marssecr.email())
|
||||
ames = GroupFactory(type_id='wg', acronym='ames', parent=area)
|
||||
ameschair = PersonFactory(user__username='ameschair')
|
||||
ames.role_set.create(name_id='chair', person=ameschair, email=ameschair.email())
|
||||
recent = GroupFactory(type_id='wg', acronym='recent', parent=area, state_id='conclude', time=a_month_ago)
|
||||
recentchair = PersonFactory(user__username='recentchair')
|
||||
recent.role_set.create(name_id='chair', person=recentchair, email=recentchair.email())
|
||||
wayold = GroupFactory(type_id='wg', acronym='wayold', parent=area, state_id='conclude', time=a_decade_ago)
|
||||
wayoldchair = PersonFactory(user__username='wayoldchair')
|
||||
wayold.role_set.create(name_id='chair', person=wayoldchair, email=wayoldchair.email())
|
||||
# create a "done" group that should not be included anywhere
|
||||
RoleFactory(name_id='ad', group__type_id='area', group__acronym='done', group__state_id='conclude')
|
||||
irtf = Group.objects.get(acronym='irtf')
|
||||
testrg = GroupFactory(type_id='rg', acronym='testrg', parent=irtf)
|
||||
testrgchair = PersonFactory(user__username='testrgchair')
|
||||
testrg.role_set.create(name_id='chair', person=testrgchair, email=testrgchair.email())
|
||||
testrag = GroupFactory(type_id='rg', acronym='testrag', parent=irtf)
|
||||
testragchair = PersonFactory(user__username='testragchair')
|
||||
testrag.role_set.create(name_id='chair', person=testragchair, email=testragchair.email())
|
||||
|
||||
output = [(alias, (domains, alist)) for alias, domains, alist in GroupAliasGenerator()]
|
||||
alias_dict = dict(output)
|
||||
self.maxDiff = None
|
||||
self.assertEqual(len(alias_dict), len(output)) # no duplicate aliases
|
||||
expected_dict = {
|
||||
area.acronym + "-ads": (["ietf"], [ad.email_address()]),
|
||||
area.acronym + "-chairs": (["ietf"], [ad.email_address(), marschair.email_address(), marssecr.email_address(), ameschair.email_address()]),
|
||||
mars.acronym + "-ads": (["ietf"], [ad.email_address()]),
|
||||
mars.acronym + "-chairs": (["ietf"], [marschair.email_address(), marssecr.email_address()]),
|
||||
ames.acronym + "-ads": (["ietf"], [ad.email_address()]),
|
||||
ames.acronym + "-chairs": (["ietf"], [ameschair.email_address()]),
|
||||
recent.acronym + "-ads": (["ietf"], [ad.email_address()]),
|
||||
recent.acronym + "-chairs": (["ietf"], [recentchair.email_address()]),
|
||||
testrg.acronym + "-chairs": (["ietf", "irtf"], [testrgchair.email_address()]),
|
||||
testrag.acronym + "-chairs": (["ietf", "irtf"], [testragchair.email_address()]),
|
||||
}
|
||||
# Sort lists for comparison
|
||||
self.assertEqual(
|
||||
{k: (sorted(doms), sorted(addrs)) for k, (doms, addrs) in alias_dict.items()},
|
||||
{k: (sorted(doms), sorted(addrs)) for k, (doms, addrs) in expected_dict.items()},
|
||||
)
|
||||
|
||||
|
||||
class GroupRoleEmailTests(TestCase):
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# Copyright The IETF Trust 2012-2023, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.urls import reverse as urlreverse
|
||||
|
@ -353,3 +354,74 @@ def update_role_set(group, role_name, new_value, by):
|
|||
e.save()
|
||||
|
||||
return added, removed
|
||||
|
||||
|
||||
class GroupAliasGenerator:
|
||||
days = 5 * 365
|
||||
active_states = ["active", "bof", "proposed"]
|
||||
group_types = [
|
||||
"wg",
|
||||
"rg",
|
||||
"rag",
|
||||
"dir",
|
||||
"team",
|
||||
"review",
|
||||
"program",
|
||||
"rfcedtyp",
|
||||
"edappr",
|
||||
"edwg",
|
||||
] # This should become groupfeature driven...
|
||||
no_ad_group_types = ["rg", "rag", "team", "program", "rfcedtyp", "edappr", "edwg"]
|
||||
|
||||
def __iter__(self):
|
||||
show_since = timezone.now() - datetime.timedelta(days=self.days)
|
||||
|
||||
# Loop through each group type and build -ads and -chairs entries
|
||||
for g in self.group_types:
|
||||
domains = ["ietf"]
|
||||
if g in ("rg", "rag"):
|
||||
domains.append("irtf")
|
||||
if g == "program":
|
||||
domains.append("iab")
|
||||
|
||||
entries = Group.objects.filter(type=g).all()
|
||||
active_entries = entries.filter(state__in=self.active_states)
|
||||
inactive_recent_entries = entries.exclude(
|
||||
state__in=self.active_states
|
||||
).filter(time__gte=show_since)
|
||||
interesting_entries = active_entries | inactive_recent_entries
|
||||
|
||||
for e in interesting_entries.distinct().iterator():
|
||||
name = e.acronym
|
||||
|
||||
# Research groups, teams, and programs do not have -ads lists
|
||||
if not g in self.no_ad_group_types:
|
||||
ad_emails = get_group_ad_emails(e)
|
||||
if ad_emails:
|
||||
yield name + "-ads", domains, list(ad_emails)
|
||||
# All group types have -chairs lists
|
||||
chair_emails = get_group_role_emails(e, ["chair", "secr"])
|
||||
if chair_emails:
|
||||
yield name + "-chairs", domains, list(chair_emails)
|
||||
|
||||
# The area lists include every chair in active working groups in the area
|
||||
areas = Group.objects.filter(type="area").all()
|
||||
active_areas = areas.filter(state__in=self.active_states)
|
||||
for area in active_areas:
|
||||
name = area.acronym
|
||||
area_ad_emails = get_group_role_emails(area, ["pre-ad", "ad", "chair"])
|
||||
if area_ad_emails:
|
||||
yield name + "-ads", ["ietf"], list(area_ad_emails)
|
||||
chair_emails = get_child_group_role_emails(area, ["chair", "secr"]) | area_ad_emails
|
||||
if chair_emails:
|
||||
yield name + "-chairs", ["ietf"], list(chair_emails)
|
||||
|
||||
# Other groups with chairs that require Internet-Draft submission approval
|
||||
gtypes = GroupTypeName.objects.values_list("slug", flat=True)
|
||||
special_groups = Group.objects.filter(
|
||||
type__features__req_subm_approval=True, acronym__in=gtypes, state="active"
|
||||
)
|
||||
for group in special_groups:
|
||||
chair_emails = get_group_role_emails(group, ["chair", "delegate"])
|
||||
if chair_emails:
|
||||
yield group.acronym + "-chairs", ["ietf"], list(chair_emails)
|
||||
|
|
85
ietf/idindex/tasks.py
Normal file
85
ietf/idindex/tasks.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
#
|
||||
# Celery task definitions
|
||||
#
|
||||
import shutil
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from celery import shared_task
|
||||
from contextlib import AbstractContextManager
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
from .index import all_id_txt, all_id2_txt, id_index_txt
|
||||
|
||||
|
||||
class TempFileManager(AbstractContextManager):
|
||||
def __init__(self, tmpdir=None) -> None:
|
||||
self.cleanup_list: set[Path] = set()
|
||||
self.dir = tmpdir
|
||||
|
||||
def make_temp_file(self, content):
|
||||
with NamedTemporaryFile(mode="wt", delete=False, dir=self.dir) as tf:
|
||||
tf_path = Path(tf.name)
|
||||
self.cleanup_list.add(tf_path)
|
||||
tf.write(content)
|
||||
return tf_path
|
||||
|
||||
def move_into_place(self, src_path: Path, dest_path: Path):
|
||||
shutil.move(src_path, dest_path)
|
||||
dest_path.chmod(0o644)
|
||||
self.cleanup_list.remove(src_path)
|
||||
|
||||
def cleanup(self):
|
||||
for tf_path in self.cleanup_list:
|
||||
tf_path.unlink(missing_ok=True)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.cleanup()
|
||||
return False # False: do not suppress the exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def idindex_update_task():
|
||||
"""Update I-D indexes"""
|
||||
id_path = Path("/a/ietfdata/doc/draft/repository")
|
||||
derived_path = Path("/a/ietfdata/derived")
|
||||
download_path = Path("/a/www/www6s/download")
|
||||
|
||||
with TempFileManager("/a/tmp") as tmp_mgr:
|
||||
# Generate copies of new contents
|
||||
all_id_content = all_id_txt()
|
||||
all_id_tmpfile = tmp_mgr.make_temp_file(all_id_content)
|
||||
derived_all_id_tmpfile = tmp_mgr.make_temp_file(all_id_content)
|
||||
download_all_id_tmpfile = tmp_mgr.make_temp_file(all_id_content)
|
||||
|
||||
id_index_content = id_index_txt()
|
||||
id_index_tmpfile = tmp_mgr.make_temp_file(id_index_content)
|
||||
derived_id_index_tmpfile = tmp_mgr.make_temp_file(id_index_content)
|
||||
download_id_index_tmpfile = tmp_mgr.make_temp_file(id_index_content)
|
||||
|
||||
id_abstracts_content = id_index_txt(with_abstracts=True)
|
||||
id_abstracts_tmpfile = tmp_mgr.make_temp_file(id_abstracts_content)
|
||||
derived_id_abstracts_tmpfile = tmp_mgr.make_temp_file(id_abstracts_content)
|
||||
download_id_abstracts_tmpfile = tmp_mgr.make_temp_file(id_abstracts_content)
|
||||
|
||||
all_id2_content = all_id2_txt()
|
||||
all_id2_tmpfile = tmp_mgr.make_temp_file(all_id2_content)
|
||||
derived_all_id2_tmpfile = tmp_mgr.make_temp_file(all_id2_content)
|
||||
|
||||
# Move temp files as-atomically-as-possible into place
|
||||
tmp_mgr.move_into_place(all_id_tmpfile, id_path / "all_id.txt")
|
||||
tmp_mgr.move_into_place(derived_all_id_tmpfile, derived_path / "all_id.txt")
|
||||
tmp_mgr.move_into_place(download_all_id_tmpfile, download_path / "id-all.txt")
|
||||
|
||||
tmp_mgr.move_into_place(id_index_tmpfile, id_path / "1id-index.txt")
|
||||
tmp_mgr.move_into_place(derived_id_index_tmpfile, derived_path / "1id-index.txt")
|
||||
tmp_mgr.move_into_place(download_id_index_tmpfile, download_path / "id-index.txt")
|
||||
|
||||
tmp_mgr.move_into_place(id_abstracts_tmpfile, id_path / "1id-abstracts.txt")
|
||||
tmp_mgr.move_into_place(derived_id_abstracts_tmpfile, derived_path / "1id-abstracts.txt")
|
||||
tmp_mgr.move_into_place(download_id_abstracts_tmpfile, download_path / "id-abstract.txt")
|
||||
|
||||
tmp_mgr.move_into_place(all_id2_tmpfile, id_path / "all_id2.txt")
|
||||
tmp_mgr.move_into_place(derived_all_id2_tmpfile, derived_path / "all_id2.txt")
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
|
||||
import datetime
|
||||
import mock
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
@ -16,6 +18,7 @@ from ietf.doc.models import Document, RelatedDocument, State, LastCallDocEvent,
|
|||
from ietf.group.factories import GroupFactory
|
||||
from ietf.name.models import DocRelationshipName
|
||||
from ietf.idindex.index import all_id_txt, all_id2_txt, id_index_txt
|
||||
from ietf.idindex.tasks import idindex_update_task, TempFileManager
|
||||
from ietf.person.factories import PersonFactory, EmailFactory
|
||||
from ietf.utils.test_utils import TestCase
|
||||
|
||||
|
@ -151,3 +154,51 @@ class IndexTests(TestCase):
|
|||
txt = id_index_txt(with_abstracts=True)
|
||||
|
||||
self.assertTrue(draft.abstract[:20] in txt)
|
||||
|
||||
|
||||
class TaskTests(TestCase):
|
||||
@mock.patch("ietf.idindex.tasks.all_id_txt")
|
||||
@mock.patch("ietf.idindex.tasks.all_id2_txt")
|
||||
@mock.patch("ietf.idindex.tasks.id_index_txt")
|
||||
@mock.patch.object(TempFileManager, "__enter__")
|
||||
def test_idindex_update_task(
|
||||
self,
|
||||
temp_file_mgr_enter_mock,
|
||||
id_index_mock,
|
||||
all_id2_mock,
|
||||
all_id_mock,
|
||||
):
|
||||
# Replace TempFileManager's __enter__() method with one that returns a mock.
|
||||
# Pass a spec to the mock so we validate that only actual methods are called.
|
||||
mgr_mock = mock.Mock(spec=TempFileManager)
|
||||
temp_file_mgr_enter_mock.return_value = mgr_mock
|
||||
|
||||
idindex_update_task()
|
||||
|
||||
self.assertEqual(all_id_mock.call_count, 1)
|
||||
self.assertEqual(all_id2_mock.call_count, 1)
|
||||
self.assertEqual(id_index_mock.call_count, 2)
|
||||
self.assertEqual(id_index_mock.call_args_list[0], (tuple(), dict()))
|
||||
self.assertEqual(
|
||||
id_index_mock.call_args_list[1],
|
||||
(tuple(), {"with_abstracts": True}),
|
||||
)
|
||||
self.assertEqual(mgr_mock.make_temp_file.call_count, 11)
|
||||
self.assertEqual(mgr_mock.move_into_place.call_count, 11)
|
||||
|
||||
def test_temp_file_manager(self):
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
with TempFileManager(temp_path) as tfm:
|
||||
path1 = tfm.make_temp_file("yay")
|
||||
path2 = tfm.make_temp_file("boo") # do not keep this one
|
||||
self.assertTrue(path1.exists())
|
||||
self.assertTrue(path2.exists())
|
||||
dest = temp_path / "yay.txt"
|
||||
tfm.move_into_place(path1, dest)
|
||||
# make sure things were cleaned up...
|
||||
self.assertFalse(path1.exists()) # moved to dest
|
||||
self.assertFalse(path2.exists()) # left behind
|
||||
# check destination contents and permissions
|
||||
self.assertEqual(dest.read_text(), "yay")
|
||||
self.assertEqual(dest.stat().st_mode & 0o777, 0o644)
|
||||
|
|
|
@ -37,7 +37,6 @@ from ietf.group.factories import GroupFactory, RoleFactory
|
|||
from ietf.group.models import Group, Role, RoleName
|
||||
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.mailinglists.models import Subscribed
|
||||
from ietf.meeting.factories import MeetingFactory
|
||||
from ietf.nomcom.factories import NomComFactory
|
||||
from ietf.person.factories import PersonFactory, EmailFactory, UserFactory, PersonalApiKeyFactory
|
||||
|
@ -227,41 +226,8 @@ class IetfAuthTests(TestCase):
|
|||
|
||||
self.assertTrue(self.username_in_htpasswd_file(email))
|
||||
|
||||
def test_create_allowlisted_account(self):
|
||||
email = "new-account@example.com"
|
||||
|
||||
# add allowlist entry
|
||||
r = self.client.post(urlreverse(ietf.ietfauth.views.login), {"username":"secretary", "password":"secretary+password"})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.profile))
|
||||
|
||||
r = self.client.get(urlreverse(ietf.ietfauth.views.add_account_allowlist))
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, "Add an allowlist entry")
|
||||
|
||||
r = self.client.post(urlreverse(ietf.ietfauth.views.add_account_allowlist), {"email": email})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertContains(r, "Allowlist entry creation successful")
|
||||
|
||||
# log out
|
||||
r = self.client.post(urlreverse('django.contrib.auth.views.logout'), {})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# register and verify allowlisted email
|
||||
self.register_and_verify(email)
|
||||
|
||||
|
||||
def test_create_subscribed_account(self):
|
||||
# verify creation with email in subscribed list
|
||||
saved_delay = settings.LIST_ACCOUNT_DELAY
|
||||
settings.LIST_ACCOUNT_DELAY = 1
|
||||
email = "subscribed@example.com"
|
||||
s = Subscribed(email=email)
|
||||
s.save()
|
||||
time.sleep(1.1)
|
||||
self.register_and_verify(email)
|
||||
settings.LIST_ACCOUNT_DELAY = saved_delay
|
||||
|
||||
# This also tests new account creation.
|
||||
def test_create_existing_account(self):
|
||||
# create account once
|
||||
email = "new-account@example.com"
|
||||
|
|
|
@ -24,5 +24,4 @@ urlpatterns = [
|
|||
url(r'^review/$', views.review_overview),
|
||||
url(r'^testemail/$', views.test_email),
|
||||
url(r'^username/$', views.change_username),
|
||||
url(r'^allowlist/add/?$', views.add_account_allowlist),
|
||||
]
|
||||
|
|
|
@ -63,11 +63,10 @@ import debug # pyflakes:ignore
|
|||
|
||||
from ietf.group.models import Role, Group
|
||||
from ietf.ietfauth.forms import ( RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm,
|
||||
AllowlistForm, ChangePasswordForm, get_person_form, RoleEmailForm,
|
||||
ChangePasswordForm, get_person_form, RoleEmailForm,
|
||||
NewEmailForm, ChangeUsernameForm, PersonPasswordForm)
|
||||
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
||||
from ietf.ietfauth.utils import role_required, has_role
|
||||
from ietf.mailinglists.models import Allowlisted
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.name.models import ExtResourceName
|
||||
from ietf.nomcom.models import NomCom
|
||||
from ietf.person.models import Person, Email, Alias, PersonalApiKey, PERSON_API_KEY_VALUES
|
||||
|
@ -160,18 +159,8 @@ def create_account(request):
|
|||
)
|
||||
new_account_email = None # Indicate to the template that we failed to create the requested account
|
||||
else:
|
||||
# For the IETF 113 Registration period (at least) we are lowering the
|
||||
# barriers for account creation to the simple email round-trip check
|
||||
send_account_creation_email(request, new_account_email)
|
||||
|
||||
# The following is what to revert to should that lowered barrier prove problematic
|
||||
# existing = Subscribed.objects.filter(email__iexact=new_account_email).first()
|
||||
# ok_to_create = ( Allowlisted.objects.filter(email__iexact=new_account_email).exists()
|
||||
# or existing and (existing.time + TimeDelta(seconds=settings.LIST_ACCOUNT_DELAY)) < DateTime.now() )
|
||||
# if ok_to_create:
|
||||
# send_account_creation_email(request, new_account_email)
|
||||
# else:
|
||||
# return render(request, 'registration/manual.html', { 'account_request_email': settings.ACCOUNT_REQUEST_EMAIL })
|
||||
else:
|
||||
form = RegistrationForm()
|
||||
|
||||
|
@ -610,23 +599,7 @@ def test_email(request):
|
|||
|
||||
return r
|
||||
|
||||
@role_required('Secretariat')
|
||||
def add_account_allowlist(request):
|
||||
success = False
|
||||
if request.method == 'POST':
|
||||
form = AllowlistForm(request.POST)
|
||||
if form.is_valid():
|
||||
email = form.cleaned_data['email']
|
||||
entry = Allowlisted(email=email, by=request.user.person)
|
||||
entry.save()
|
||||
success = True
|
||||
else:
|
||||
form = AllowlistForm()
|
||||
|
||||
return render(request, 'ietfauth/allowlist_form.html', {
|
||||
'form': form,
|
||||
'success': success,
|
||||
})
|
||||
|
||||
class AddReviewWishForm(forms.Form):
|
||||
doc = SearchableDocumentField(label="Document", doc_type="draft")
|
||||
|
|
|
@ -2,20 +2,15 @@
|
|||
|
||||
from django.contrib import admin
|
||||
|
||||
from ietf.mailinglists.models import List, Subscribed, Allowlisted
|
||||
from ietf.mailinglists.models import NonWgMailingList, Allowlisted
|
||||
|
||||
|
||||
class ListAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'name', 'description', 'advertised')
|
||||
|
||||
|
||||
class NonWgMailingListAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'name', 'description')
|
||||
search_fields = ('name',)
|
||||
admin.site.register(List, ListAdmin)
|
||||
|
||||
|
||||
class SubscribedAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'time', 'email')
|
||||
raw_id_fields = ('lists',)
|
||||
search_fields = ('email',)
|
||||
admin.site.register(Subscribed, SubscribedAdmin)
|
||||
admin.site.register(NonWgMailingList, NonWgMailingListAdmin)
|
||||
|
||||
|
||||
class AllowlistedAdmin(admin.ModelAdmin):
|
||||
|
|
|
@ -3,16 +3,14 @@
|
|||
|
||||
|
||||
import factory
|
||||
import random
|
||||
|
||||
from ietf.mailinglists.models import List
|
||||
from ietf.mailinglists.models import NonWgMailingList
|
||||
|
||||
class ListFactory(factory.django.DjangoModelFactory):
|
||||
class NonWgMailingListFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = List
|
||||
model = NonWgMailingList
|
||||
|
||||
name = factory.Sequence(lambda n: "list-name-%s" % n)
|
||||
description = factory.Faker('sentence', nb_words=10)
|
||||
advertised = factory.LazyAttribute(lambda obj: random.randint(0, 1))
|
||||
|
||||
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
# Copyright The IETF Trust 2016-2019, All Rights Reserved
|
||||
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
from textwrap import dedent
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
|
||||
|
||||
from ietf.mailinglists.models import List, Subscribed
|
||||
from ietf.utils.log import log
|
||||
|
||||
mark = time.time()
|
||||
|
||||
def import_mailman_listinfo(verbosity=0):
|
||||
def note(msg):
|
||||
if verbosity > 2:
|
||||
sys.stdout.write(msg)
|
||||
sys.stdout.write('\n')
|
||||
def log_time(msg):
|
||||
global mark
|
||||
if verbosity > 1:
|
||||
t = time.time()
|
||||
log(msg+' (%.1fs)'% (t-mark))
|
||||
mark = t
|
||||
|
||||
cmd = str(Path(settings.BASE_DIR) / "bin" / "mailman_listinfo.py")
|
||||
result = subprocess.run([cmd], capture_output=True)
|
||||
if result.stderr:
|
||||
log("Error exporting information from mailmain")
|
||||
log(result.stderr)
|
||||
return
|
||||
mailman_export = json.loads(result.stdout)
|
||||
|
||||
names = sorted(mailman_export.keys())
|
||||
addr_max_length = Subscribed._meta.get_field('email').max_length
|
||||
|
||||
subscribed = { l.name: set(l.subscribed_set.values_list('email', flat=True)) for l in List.objects.all().prefetch_related('subscribed_set') }
|
||||
|
||||
for name in names:
|
||||
note("List: %s" % mailman_export[name]['internal_name'])
|
||||
|
||||
lists = List.objects.filter(name=mailman_export[name]['real_name'])
|
||||
if lists.count() > 1:
|
||||
# Arbitrary choice; we'll update the remaining item next
|
||||
for item in lists[1:]:
|
||||
item.delete()
|
||||
mmlist, created = List.objects.get_or_create(name=mailman_export[name]['real_name'])
|
||||
dirty = False
|
||||
desc = mailman_export[name]['description'][:256]
|
||||
if mmlist.description != desc:
|
||||
mmlist.description = desc
|
||||
dirty = True
|
||||
if mmlist.advertised != mailman_export[name]['advertised']:
|
||||
mmlist.advertised = mailman_export[name]['advertised']
|
||||
dirty = True
|
||||
if dirty:
|
||||
mmlist.save()
|
||||
# The following calls return lowercased addresses
|
||||
if mailman_export[name]['advertised']:
|
||||
members = set(mailman_export[name]['members'])
|
||||
if not mailman_export[name]['real_name'] in subscribed:
|
||||
# 2022-7-29: lots of these going into the logs but being ignored...
|
||||
# log("Note: didn't find '%s' in the dictionary of subscriptions" % mailman_export[name]['real_name'])
|
||||
continue
|
||||
known = subscribed[mailman_export[name]['real_name']]
|
||||
log_time(" Fetched known list members from database")
|
||||
to_remove = known - members
|
||||
to_add = members - known
|
||||
for addr in to_remove:
|
||||
note(" Removing subscription: %s" % (addr))
|
||||
old = Subscribed.objects.get(email=addr) # Intentionally leaving this as case-sensitive in postgres
|
||||
old.lists.remove(mmlist)
|
||||
if old.lists.count() == 0:
|
||||
note(" Removing address with no subscriptions: %s" % (addr))
|
||||
old.delete()
|
||||
if to_remove:
|
||||
log(" Removed %s addresses from %s" % (len(to_remove), name))
|
||||
for addr in to_add:
|
||||
if len(addr) > addr_max_length:
|
||||
sys.stderr.write(" ** Email address subscribed to '%s' too long for table: <%s>\n" % (name, addr))
|
||||
continue
|
||||
note(" Adding subscription: %s" % (addr))
|
||||
try:
|
||||
new, created = Subscribed.objects.get_or_create(email=addr) # Intentionally leaving this as case-sensitive in postgres
|
||||
except MultipleObjectsReturned as e:
|
||||
sys.stderr.write(" ** Error handling %s in %s: %s\n" % (addr, name, e))
|
||||
continue
|
||||
new.lists.add(mmlist)
|
||||
if to_add:
|
||||
log(" Added %s addresses to %s" % (len(to_add), name))
|
||||
log("Completed import of list info from Mailman")
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Import list information from Mailman.
|
||||
|
||||
Import announced list names, descriptions, and subscribers, by calling the
|
||||
appropriate Mailman functions and adding entries to the database.
|
||||
|
||||
Run this from cron regularly, with sufficient permissions to access the
|
||||
mailman database files.
|
||||
|
||||
"""
|
||||
|
||||
help = dedent(__doc__).strip()
|
||||
|
||||
#option_list = BaseCommand.option_list + ( )
|
||||
|
||||
|
||||
def handle(self, *filenames, **options):
|
||||
"""
|
||||
|
||||
* Import announced lists, with appropriate meta-information.
|
||||
|
||||
* For each list, import the members.
|
||||
|
||||
"""
|
||||
|
||||
verbosity = int(options.get('verbosity'))
|
||||
|
||||
import_mailman_listinfo(verbosity)
|
628
ietf/mailinglists/migrations/0002_nonwgmailinglist.py
Normal file
628
ietf/mailinglists/migrations/0002_nonwgmailinglist.py
Normal file
|
@ -0,0 +1,628 @@
|
|||
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
NonWgMailingList = apps.get_model("mailinglists", "NonWgMailingList")
|
||||
List = apps.get_model("mailinglists", "List")
|
||||
|
||||
for l in List.objects.filter(
|
||||
pk__in=[
|
||||
10754,
|
||||
10769,
|
||||
10770,
|
||||
10768,
|
||||
10787,
|
||||
10785,
|
||||
10791,
|
||||
10786,
|
||||
10816,
|
||||
10817,
|
||||
10819,
|
||||
10818,
|
||||
10922,
|
||||
10923,
|
||||
10921,
|
||||
10940,
|
||||
10941,
|
||||
10942,
|
||||
572,
|
||||
10297,
|
||||
182,
|
||||
43,
|
||||
10704,
|
||||
10314,
|
||||
201,
|
||||
419,
|
||||
282,
|
||||
149,
|
||||
223,
|
||||
10874,
|
||||
10598,
|
||||
10639,
|
||||
10875,
|
||||
10737,
|
||||
105,
|
||||
65,
|
||||
10781,
|
||||
10771,
|
||||
10946,
|
||||
518,
|
||||
421,
|
||||
214,
|
||||
285,
|
||||
393,
|
||||
445,
|
||||
553,
|
||||
183,
|
||||
10725,
|
||||
33,
|
||||
10766,
|
||||
114,
|
||||
417,
|
||||
10789,
|
||||
10876,
|
||||
4244,
|
||||
10705,
|
||||
10706,
|
||||
10878,
|
||||
10324,
|
||||
10879,
|
||||
10642,
|
||||
10821,
|
||||
547,
|
||||
532,
|
||||
10636,
|
||||
10592,
|
||||
327,
|
||||
248,
|
||||
10697,
|
||||
288,
|
||||
346,
|
||||
10731,
|
||||
10955,
|
||||
10857,
|
||||
446,
|
||||
55,
|
||||
10799,
|
||||
10800,
|
||||
10801,
|
||||
10612,
|
||||
73,
|
||||
3,
|
||||
358,
|
||||
9640,
|
||||
10868,
|
||||
378,
|
||||
462,
|
||||
6595,
|
||||
10914,
|
||||
10915,
|
||||
197,
|
||||
63,
|
||||
558,
|
||||
10824,
|
||||
124,
|
||||
10881,
|
||||
177,
|
||||
312,
|
||||
252,
|
||||
185,
|
||||
523,
|
||||
4572,
|
||||
10618,
|
||||
206,
|
||||
68,
|
||||
10859,
|
||||
560,
|
||||
513,
|
||||
246,
|
||||
7817,
|
||||
148,
|
||||
10864,
|
||||
10589,
|
||||
10773,
|
||||
10748,
|
||||
364,
|
||||
311,
|
||||
10302,
|
||||
10272,
|
||||
10929,
|
||||
171,
|
||||
10865,
|
||||
10919,
|
||||
377,
|
||||
469,
|
||||
467,
|
||||
411,
|
||||
505,
|
||||
6318,
|
||||
10811,
|
||||
10304,
|
||||
10882,
|
||||
10845,
|
||||
568,
|
||||
10883,
|
||||
4774,
|
||||
264,
|
||||
10779,
|
||||
10884,
|
||||
10303,
|
||||
409,
|
||||
10590,
|
||||
451,
|
||||
10749,
|
||||
10765,
|
||||
486,
|
||||
519,
|
||||
10593,
|
||||
10313,
|
||||
550,
|
||||
10707,
|
||||
307,
|
||||
10861,
|
||||
10654,
|
||||
10708,
|
||||
10275,
|
||||
134,
|
||||
460,
|
||||
10911,
|
||||
10574,
|
||||
10885,
|
||||
10814,
|
||||
10676,
|
||||
10747,
|
||||
10305,
|
||||
10688,
|
||||
36,
|
||||
10844,
|
||||
10620,
|
||||
458,
|
||||
10282,
|
||||
10594,
|
||||
10752,
|
||||
389,
|
||||
296,
|
||||
10684,
|
||||
48,
|
||||
533,
|
||||
443,
|
||||
10739,
|
||||
491,
|
||||
139,
|
||||
461,
|
||||
10690,
|
||||
424,
|
||||
290,
|
||||
336,
|
||||
31,
|
||||
10709,
|
||||
382,
|
||||
10866,
|
||||
10724,
|
||||
539,
|
||||
10710,
|
||||
559,
|
||||
10609,
|
||||
74,
|
||||
10582,
|
||||
133,
|
||||
10621,
|
||||
34,
|
||||
10596,
|
||||
442,
|
||||
13,
|
||||
56,
|
||||
128,
|
||||
323,
|
||||
10285,
|
||||
80,
|
||||
315,
|
||||
3520,
|
||||
10949,
|
||||
10950,
|
||||
189,
|
||||
2599,
|
||||
10822,
|
||||
164,
|
||||
10267,
|
||||
10286,
|
||||
464,
|
||||
440,
|
||||
254,
|
||||
262,
|
||||
10943,
|
||||
465,
|
||||
75,
|
||||
179,
|
||||
162,
|
||||
457,
|
||||
10572,
|
||||
372,
|
||||
452,
|
||||
10273,
|
||||
88,
|
||||
366,
|
||||
331,
|
||||
140,
|
||||
407,
|
||||
416,
|
||||
91,
|
||||
10632,
|
||||
542,
|
||||
151,
|
||||
117,
|
||||
431,
|
||||
10628,
|
||||
10271,
|
||||
14,
|
||||
540,
|
||||
278,
|
||||
352,
|
||||
159,
|
||||
10851,
|
||||
9981,
|
||||
10694,
|
||||
10619,
|
||||
10732,
|
||||
320,
|
||||
348,
|
||||
338,
|
||||
349,
|
||||
10678,
|
||||
468,
|
||||
293,
|
||||
350,
|
||||
402,
|
||||
57,
|
||||
524,
|
||||
141,
|
||||
71,
|
||||
67,
|
||||
508,
|
||||
7828,
|
||||
10268,
|
||||
10631,
|
||||
10713,
|
||||
10889,
|
||||
345,
|
||||
78,
|
||||
342,
|
||||
190,
|
||||
10869,
|
||||
46,
|
||||
334,
|
||||
255,
|
||||
5823,
|
||||
400,
|
||||
10867,
|
||||
23,
|
||||
10666,
|
||||
10685,
|
||||
405,
|
||||
2801,
|
||||
92,
|
||||
137,
|
||||
10640,
|
||||
10656,
|
||||
104,
|
||||
123,
|
||||
10643,
|
||||
10891,
|
||||
466,
|
||||
10567,
|
||||
10318,
|
||||
526,
|
||||
30,
|
||||
222,
|
||||
194,
|
||||
10735,
|
||||
10714,
|
||||
247,
|
||||
493,
|
||||
1162,
|
||||
414,
|
||||
10648,
|
||||
10677,
|
||||
126,
|
||||
16,
|
||||
422,
|
||||
271,
|
||||
295,
|
||||
81,
|
||||
10634,
|
||||
544,
|
||||
10850,
|
||||
426,
|
||||
573,
|
||||
353,
|
||||
10829,
|
||||
538,
|
||||
10913,
|
||||
10566,
|
||||
167,
|
||||
10675,
|
||||
272,
|
||||
10673,
|
||||
10767,
|
||||
528,
|
||||
284,
|
||||
564,
|
||||
268,
|
||||
10825,
|
||||
231,
|
||||
520,
|
||||
10645,
|
||||
10872,
|
||||
515,
|
||||
10956,
|
||||
10947,
|
||||
569,
|
||||
233,
|
||||
10952,
|
||||
195,
|
||||
10938,
|
||||
2809,
|
||||
10591,
|
||||
10665,
|
||||
9639,
|
||||
10775,
|
||||
10760,
|
||||
10715,
|
||||
10716,
|
||||
10667,
|
||||
361,
|
||||
184,
|
||||
10935,
|
||||
10957,
|
||||
10944,
|
||||
94,
|
||||
449,
|
||||
525,
|
||||
1962,
|
||||
10300,
|
||||
10894,
|
||||
9156,
|
||||
10774,
|
||||
256,
|
||||
289,
|
||||
218,
|
||||
187,
|
||||
40,
|
||||
10777,
|
||||
10761,
|
||||
10670,
|
||||
249,
|
||||
10764,
|
||||
420,
|
||||
548,
|
||||
232,
|
||||
410,
|
||||
196,
|
||||
72,
|
||||
335,
|
||||
70,
|
||||
146,
|
||||
10287,
|
||||
10299,
|
||||
10311,
|
||||
10895,
|
||||
10617,
|
||||
531,
|
||||
343,
|
||||
10934,
|
||||
10933,
|
||||
10597,
|
||||
158,
|
||||
10600,
|
||||
10692,
|
||||
8630,
|
||||
556,
|
||||
324,
|
||||
11,
|
||||
10784,
|
||||
498,
|
||||
10772,
|
||||
478,
|
||||
10833,
|
||||
10691,
|
||||
391,
|
||||
10565,
|
||||
10669,
|
||||
113,
|
||||
110,
|
||||
7831,
|
||||
10855,
|
||||
10312,
|
||||
10315,
|
||||
10896,
|
||||
10672,
|
||||
10306,
|
||||
438,
|
||||
395,
|
||||
82,
|
||||
10599,
|
||||
10953,
|
||||
10858,
|
||||
10807,
|
||||
10717,
|
||||
310,
|
||||
10808,
|
||||
119,
|
||||
10595,
|
||||
10718,
|
||||
10317,
|
||||
10898,
|
||||
454,
|
||||
427,
|
||||
10583,
|
||||
10916,
|
||||
403,
|
||||
10843,
|
||||
10899,
|
||||
291,
|
||||
10812,
|
||||
10900,
|
||||
10794,
|
||||
341,
|
||||
121,
|
||||
230,
|
||||
136,
|
||||
166,
|
||||
394,
|
||||
234,
|
||||
10901,
|
||||
2466,
|
||||
10573,
|
||||
10939,
|
||||
221,
|
||||
490,
|
||||
10820,
|
||||
10873,
|
||||
10792,
|
||||
10870,
|
||||
10793,
|
||||
10904,
|
||||
181,
|
||||
10693,
|
||||
482,
|
||||
10611,
|
||||
125,
|
||||
10568,
|
||||
10788,
|
||||
211,
|
||||
10756,
|
||||
10719,
|
||||
100,
|
||||
228,
|
||||
5833,
|
||||
251,
|
||||
122,
|
||||
39,
|
||||
534,
|
||||
437,
|
||||
504,
|
||||
10613,
|
||||
439,
|
||||
306,
|
||||
10863,
|
||||
10823,
|
||||
10926,
|
||||
76,
|
||||
227,
|
||||
59,
|
||||
42,
|
||||
455,
|
||||
10927,
|
||||
10928,
|
||||
204,
|
||||
430,
|
||||
10720,
|
||||
267,
|
||||
396,
|
||||
10849,
|
||||
10308,
|
||||
281,
|
||||
10905,
|
||||
10736,
|
||||
168,
|
||||
153,
|
||||
385,
|
||||
89,
|
||||
529,
|
||||
412,
|
||||
215,
|
||||
484,
|
||||
10951,
|
||||
66,
|
||||
173,
|
||||
10633,
|
||||
10681,
|
||||
3613,
|
||||
10274,
|
||||
10750,
|
||||
367,
|
||||
387,
|
||||
10832,
|
||||
35,
|
||||
147,
|
||||
10325,
|
||||
10671,
|
||||
565,
|
||||
313,
|
||||
10871,
|
||||
10751,
|
||||
37,
|
||||
10936,
|
||||
10937,
|
||||
287,
|
||||
496,
|
||||
244,
|
||||
10841,
|
||||
10683,
|
||||
10906,
|
||||
10584,
|
||||
479,
|
||||
10856,
|
||||
163,
|
||||
10910,
|
||||
257,
|
||||
276,
|
||||
10840,
|
||||
10689,
|
||||
365,
|
||||
10847,
|
||||
99,
|
||||
77,
|
||||
435,
|
||||
213,
|
||||
15,
|
||||
10932,
|
||||
58,
|
||||
10722,
|
||||
131,
|
||||
363,
|
||||
10674,
|
||||
322,
|
||||
180,
|
||||
10917,
|
||||
10918,
|
||||
10738,
|
||||
10954,
|
||||
10581,
|
||||
208,
|
||||
337,
|
||||
4,
|
||||
571,
|
||||
10668,
|
||||
10291,
|
||||
]
|
||||
):
|
||||
NonWgMailingList.objects.create(name=l.name, description=l.description)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mailinglists", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="NonWgMailingList",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=32)),
|
||||
("description", models.CharField(max_length=256)),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(forward),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 4.2.9 on 2024-02-02 23:04
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mailinglists", "0002_nonwgmailinglist"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="subscribed",
|
||||
name="lists",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="List",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="Subscribed",
|
||||
),
|
||||
]
|
|
@ -9,25 +9,20 @@ from django.db import models
|
|||
from ietf.person.models import Person
|
||||
from ietf.utils.models import ForeignKey
|
||||
|
||||
class List(models.Model):
|
||||
|
||||
# NonWgMailingList is a temporary bridging class to hold information known about mailman2
|
||||
# while decoupling from mailman2 until we integrate with mailman3
|
||||
class NonWgMailingList(models.Model):
|
||||
name = models.CharField(max_length=32)
|
||||
description = models.CharField(max_length=256)
|
||||
advertised = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return "<List: %s>" % self.name
|
||||
return "<NonWgMailingList: %s>" % self.name
|
||||
def info_url(self):
|
||||
return settings.MAILING_LIST_INFO_URL % {'list_addr': self.name }
|
||||
|
||||
class Subscribed(models.Model):
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
email = models.CharField(max_length=128, validators=[validate_email])
|
||||
lists = models.ManyToManyField(List)
|
||||
def __str__(self):
|
||||
return "<Subscribed: %s at %s>" % (self.email, self.time)
|
||||
class Meta:
|
||||
verbose_name_plural = "Subscribed"
|
||||
|
||||
# Allowlisted is unused, but is not being dropped until its human-curated content
|
||||
# is archived outside this database.
|
||||
class Allowlisted(models.Model):
|
||||
time = models.DateTimeField(auto_now_add=True)
|
||||
email = models.CharField("Email address", max_length=64, validators=[validate_email])
|
||||
|
|
|
@ -11,7 +11,7 @@ from tastypie.cache import SimpleCache
|
|||
from ietf import api
|
||||
from ietf.api import ToOneField # pyflakes:ignore
|
||||
|
||||
from ietf.mailinglists.models import Allowlisted, List, Subscribed
|
||||
from ietf.mailinglists.models import Allowlisted, NonWgMailingList
|
||||
|
||||
|
||||
from ietf.person.resources import PersonResource
|
||||
|
@ -31,34 +31,19 @@ class AllowlistedResource(ModelResource):
|
|||
}
|
||||
api.mailinglists.register(AllowlistedResource())
|
||||
|
||||
class ListResource(ModelResource):
|
||||
class NonWgMailingListResource(ModelResource):
|
||||
class Meta:
|
||||
queryset = List.objects.all()
|
||||
queryset = NonWgMailingList.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'list'
|
||||
#resource_name = 'nonwgmailinglist'
|
||||
ordering = ['id', ]
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"name": ALL,
|
||||
"description": ALL,
|
||||
"advertised": ALL,
|
||||
}
|
||||
api.mailinglists.register(ListResource())
|
||||
api.mailinglists.register(NonWgMailingListResource())
|
||||
|
||||
|
||||
class SubscribedResource(ModelResource):
|
||||
lists = ToManyField(ListResource, 'lists', null=True)
|
||||
class Meta:
|
||||
queryset = Subscribed.objects.all()
|
||||
serializer = api.Serializer()
|
||||
cache = SimpleCache()
|
||||
#resource_name = 'subscribed'
|
||||
ordering = ['id', ]
|
||||
filtering = {
|
||||
"id": ALL,
|
||||
"time": ALL,
|
||||
"email": ALL,
|
||||
"lists": ALL_WITH_RELATIONS,
|
||||
}
|
||||
api.mailinglists.register(SubscribedResource())
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.urls import reverse as urlreverse
|
|||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.group.factories import GroupFactory
|
||||
from ietf.mailinglists.factories import ListFactory
|
||||
from ietf.mailinglists.factories import NonWgMailingListFactory
|
||||
from ietf.utils.test_utils import TestCase
|
||||
|
||||
|
||||
|
@ -32,23 +32,13 @@ class MailingListTests(TestCase):
|
|||
|
||||
|
||||
def test_nonwg(self):
|
||||
groups = list()
|
||||
groups.append(GroupFactory(type_id='wg', acronym='mars', list_archive='https://ietf.org/mars'))
|
||||
groups.append(GroupFactory(type_id='wg', acronym='ames', state_id='conclude', list_archive='https://ietf.org/ames'))
|
||||
groups.append(GroupFactory(type_id='wg', acronym='newstuff', state_id='bof', list_archive='https://ietf.org/newstuff'))
|
||||
groups.append(GroupFactory(type_id='rg', acronym='research', list_archive='https://irtf.org/research'))
|
||||
lists = ListFactory.create_batch(7)
|
||||
|
||||
lists = NonWgMailingListFactory.create_batch(7)
|
||||
|
||||
url = urlreverse("ietf.mailinglists.views.nonwg")
|
||||
|
||||
r = self.client.get(url)
|
||||
for l in lists:
|
||||
if l.advertised:
|
||||
self.assertContains(r, l.name)
|
||||
self.assertContains(r, l.description)
|
||||
else:
|
||||
self.assertNotContains(r, l.name, html=True)
|
||||
self.assertNotContains(r, l.description, html=True)
|
||||
|
||||
for g in groups:
|
||||
self.assertNotContains(r, g.acronym, html=True)
|
||||
|
|
|
@ -1,33 +1,25 @@
|
|||
# Copyright The IETF Trust 2007-2022, All Rights Reserved
|
||||
|
||||
import re
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.group.models import Group
|
||||
from ietf.mailinglists.models import List
|
||||
from ietf.mailinglists.models import NonWgMailingList
|
||||
|
||||
|
||||
def groups(request):
|
||||
groups = Group.objects.filter(type__features__acts_like_wg=True, list_archive__startswith='http').exclude(state__in=('bof', 'conclude')).order_by("acronym")
|
||||
groups = (
|
||||
Group.objects.filter(
|
||||
type__features__acts_like_wg=True, list_archive__startswith="http"
|
||||
)
|
||||
.exclude(state__in=("bof", "conclude"))
|
||||
.order_by("acronym")
|
||||
)
|
||||
|
||||
return render(request, "mailinglists/group_archives.html", {"groups": groups})
|
||||
|
||||
return render(request, "mailinglists/group_archives.html", { "groups": groups } )
|
||||
|
||||
def nonwg(request):
|
||||
groups = Group.objects.filter(type__features__acts_like_wg=True).exclude(state__in=['bof']).order_by("acronym")
|
||||
|
||||
#urls = [ g.list_archive for g in groups if '.ietf.org' in g.list_archive ]
|
||||
|
||||
wg_lists = set()
|
||||
for g in groups:
|
||||
wg_lists.add(g.acronym)
|
||||
match = re.search(r'^(https?://mailarchive.ietf.org/arch/(browse/|search/\?email-list=))(?P<name>[^/]*)/?$', g.list_archive)
|
||||
if match:
|
||||
wg_lists.add(match.group('name').lower())
|
||||
|
||||
lists = List.objects.filter(advertised=True)
|
||||
#debug.show('lists.count()')
|
||||
lists = lists.exclude(name__in=wg_lists).order_by('name')
|
||||
#debug.show('lists.count()')
|
||||
return render(request, "mailinglists/nonwg.html", { "lists": lists } )
|
||||
lists = NonWgMailingList.objects.order_by("name")
|
||||
return render(request, "mailinglists/nonwg.html", {"lists": lists})
|
||||
|
|
|
@ -103,16 +103,7 @@
|
|||
Joint session with:<br>
|
||||
(To request one session for multiple WGs together.)
|
||||
</td>
|
||||
<td>{{ form.joint_with_groups_selector }}
|
||||
<button type="button" onclick="ietf_sessions.delete_last_joint_with_groups(); return 1;">Delete the last entry</button><br>
|
||||
{{ form.joint_with_groups.errors }}{{ form.joint_with_groups }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg1">
|
||||
<td>
|
||||
Of the sessions requested by this WG, the joint session, if applicable, is:
|
||||
</td>
|
||||
<td>{{ form.joint_for_session.errors }}{{ form.joint_for_session }}</td>
|
||||
<td>To request a joint session with another group, please contact the secretariat.</td>
|
||||
</tr>
|
||||
|
||||
{% endif %}
|
||||
|
|
|
@ -169,8 +169,8 @@ if SERVER_MODE != 'production' and SERVE_CDN_FILES_LOCALLY_IN_DEV_MODE:
|
|||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = os.path.abspath(BASE_DIR + "/../static/")
|
||||
else:
|
||||
STATIC_URL = "https://static.ietf.org/lib/%s/"%__version__
|
||||
# Intentionally not setting STATIC_ROOT - see django/django (the default is None)
|
||||
STATIC_URL = "https://static.ietf.org/dt/%s/"%__version__
|
||||
STATIC_ROOT = "/a/www/www6s/lib/dt/%s/"%__version__
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
|
|
|
@ -5,11 +5,14 @@
|
|||
import datetime
|
||||
import io
|
||||
import requests
|
||||
|
||||
from celery import shared_task
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from ietf.sync.rfceditor import MIN_ERRATA_RESULTS, MIN_INDEX_RESULTS, parse_index, update_docs_from_rfc_index
|
||||
from ietf.sync import iana
|
||||
from ietf.sync import rfceditor
|
||||
from ietf.utils import log
|
||||
from ietf.utils.timezone import date_today
|
||||
|
||||
|
@ -44,7 +47,7 @@ def rfc_editor_index_update_task(full_index=False):
|
|||
log.log(f'GET request timed out retrieving RFC editor index: {exc}')
|
||||
return # failed
|
||||
rfc_index_xml = response.text
|
||||
index_data = parse_index(io.StringIO(rfc_index_xml))
|
||||
index_data = rfceditor.parse_index(io.StringIO(rfc_index_xml))
|
||||
try:
|
||||
response = requests.get(
|
||||
settings.RFC_EDITOR_ERRATA_JSON_URL,
|
||||
|
@ -54,14 +57,98 @@ def rfc_editor_index_update_task(full_index=False):
|
|||
log.log(f'GET request timed out retrieving RFC editor errata: {exc}')
|
||||
return # failed
|
||||
errata_data = response.json()
|
||||
if len(index_data) < MIN_INDEX_RESULTS:
|
||||
if len(index_data) < rfceditor.MIN_INDEX_RESULTS:
|
||||
log.log("Not enough index entries, only %s" % len(index_data))
|
||||
return # failed
|
||||
if len(errata_data) < MIN_ERRATA_RESULTS:
|
||||
if len(errata_data) < rfceditor.MIN_ERRATA_RESULTS:
|
||||
log.log("Not enough errata entries, only %s" % len(errata_data))
|
||||
return # failed
|
||||
for rfc_number, changes, doc, rfc_published in update_docs_from_rfc_index(
|
||||
for rfc_number, changes, doc, rfc_published in rfceditor.update_docs_from_rfc_index(
|
||||
index_data, errata_data, skip_older_than_date=skip_date
|
||||
):
|
||||
for c in changes:
|
||||
log.log("RFC%s, %s: %s" % (rfc_number, doc.name, c))
|
||||
|
||||
|
||||
@shared_task
|
||||
def iana_changes_update_task():
|
||||
# compensate to avoid we ask for something that happened now and then
|
||||
# don't get it back because our request interval is slightly off
|
||||
CLOCK_SKEW_COMPENSATION = 5 # seconds
|
||||
|
||||
# actually the interface accepts 24 hours, but then we get into
|
||||
# trouble with daylights savings - meh
|
||||
MAX_INTERVAL_ACCEPTED_BY_IANA = datetime.timedelta(hours=23)
|
||||
|
||||
start = (
|
||||
timezone.now()
|
||||
- datetime.timedelta(hours=23)
|
||||
+ datetime.timedelta(seconds=CLOCK_SKEW_COMPENSATION,)
|
||||
)
|
||||
end = start + datetime.timedelta(hours=23)
|
||||
|
||||
t = start
|
||||
while t < end:
|
||||
# the IANA server doesn't allow us to fetch more than a certain
|
||||
# period, so loop over the requested period and make multiple
|
||||
# requests if necessary
|
||||
|
||||
text = iana.fetch_changes_json(
|
||||
settings.IANA_SYNC_CHANGES_URL, t, min(end, t + MAX_INTERVAL_ACCEPTED_BY_IANA)
|
||||
)
|
||||
log.log(f"Retrieved the JSON: {text}")
|
||||
|
||||
changes = iana.parse_changes_json(text)
|
||||
added_events, warnings = iana.update_history_with_changes(
|
||||
changes, send_email=True
|
||||
)
|
||||
|
||||
for e in added_events:
|
||||
log.log(
|
||||
f"Added event for {e.doc_id} {e.time}: {e.desc} (parsed json: {e.json})"
|
||||
)
|
||||
|
||||
for w in warnings:
|
||||
log.log(f"WARNING: {w}")
|
||||
|
||||
t += MAX_INTERVAL_ACCEPTED_BY_IANA
|
||||
|
||||
|
||||
@shared_task
|
||||
def iana_protocols_update_task():
|
||||
# Earliest date for which we have data suitable to update (was described as
|
||||
# "this needs to be the date where this tool is first deployed" in the original
|
||||
# iana-protocols-updates script)"
|
||||
rfc_must_published_later_than = datetime.datetime(
|
||||
2012,
|
||||
11,
|
||||
26,
|
||||
tzinfo=datetime.timezone.utc,
|
||||
)
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
settings.IANA_SYNC_PROTOCOLS_URL,
|
||||
timeout=30,
|
||||
)
|
||||
except requests.Timeout as exc:
|
||||
log.log(f'GET request timed out retrieving IANA protocols page: {exc}')
|
||||
return
|
||||
|
||||
rfc_numbers = iana.parse_protocol_page(response.text)
|
||||
|
||||
def batched(l, n):
|
||||
"""Split list l up in batches of max size n.
|
||||
|
||||
For Python 3.12 or later, replace this with itertools.batched()
|
||||
"""
|
||||
return (l[i:i + n] for i in range(0, len(l), n))
|
||||
|
||||
for batch in batched(rfc_numbers, 100):
|
||||
updated = iana.update_rfc_log_from_protocol_page(
|
||||
batch,
|
||||
rfc_must_published_later_than,
|
||||
)
|
||||
|
||||
for d in updated:
|
||||
log.log("Added history entry for %s" % d.display_name())
|
||||
|
|
|
@ -19,7 +19,7 @@ from django.test.utils import override_settings
|
|||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.doc.factories import WgDraftFactory, RfcFactory, DocumentAuthorFactory
|
||||
from ietf.doc.factories import WgDraftFactory, RfcFactory, DocumentAuthorFactory, DocEventFactory
|
||||
from ietf.doc.models import Document, DocEvent, DeletedEvent, DocTagName, RelatedDocument, State, StateDocEvent
|
||||
from ietf.doc.utils import add_state_change_event
|
||||
from ietf.group.factories import GroupFactory
|
||||
|
@ -685,8 +685,8 @@ class TaskTests(TestCase):
|
|||
RFC_EDITOR_INDEX_URL="https://rfc-editor.example.com/index/",
|
||||
RFC_EDITOR_ERRATA_JSON_URL="https://rfc-editor.example.com/errata/",
|
||||
)
|
||||
@mock.patch("ietf.sync.tasks.update_docs_from_rfc_index")
|
||||
@mock.patch("ietf.sync.tasks.parse_index")
|
||||
@mock.patch("ietf.sync.tasks.rfceditor.update_docs_from_rfc_index")
|
||||
@mock.patch("ietf.sync.tasks.rfceditor.parse_index")
|
||||
@mock.patch("ietf.sync.tasks.requests.get")
|
||||
def test_rfc_editor_index_update_task(
|
||||
self, requests_get_mock, parse_index_mock, update_docs_mock
|
||||
|
@ -804,3 +804,102 @@ class TaskTests(TestCase):
|
|||
parse_index_mock.return_value = MockIndexData(length=rfceditor.MIN_INDEX_RESULTS)
|
||||
tasks.rfc_editor_index_update_task(full_index=False)
|
||||
self.assertFalse(update_docs_mock.called)
|
||||
|
||||
@override_settings(IANA_SYNC_CHANGES_URL="https://iana.example.com/sync/")
|
||||
@mock.patch("ietf.sync.tasks.iana.update_history_with_changes")
|
||||
@mock.patch("ietf.sync.tasks.iana.parse_changes_json")
|
||||
@mock.patch("ietf.sync.tasks.iana.fetch_changes_json")
|
||||
def test_iana_changes_update_task(
|
||||
self,
|
||||
fetch_changes_mock,
|
||||
parse_changes_mock,
|
||||
update_history_mock,
|
||||
):
|
||||
# set up mocks
|
||||
fetch_return_val = object()
|
||||
fetch_changes_mock.return_value = fetch_return_val
|
||||
parse_return_val = object()
|
||||
parse_changes_mock.return_value = parse_return_val
|
||||
event_with_json = DocEventFactory()
|
||||
event_with_json.json = "hi I'm json"
|
||||
update_history_mock.return_value = [
|
||||
[event_with_json], # events
|
||||
["oh no!"], # warnings
|
||||
]
|
||||
|
||||
tasks.iana_changes_update_task()
|
||||
self.assertEqual(fetch_changes_mock.call_count, 1)
|
||||
self.assertEqual(
|
||||
fetch_changes_mock.call_args[0][0],
|
||||
"https://iana.example.com/sync/",
|
||||
)
|
||||
self.assertTrue(parse_changes_mock.called)
|
||||
self.assertEqual(
|
||||
parse_changes_mock.call_args,
|
||||
((fetch_return_val,), {}),
|
||||
)
|
||||
self.assertTrue(update_history_mock.called)
|
||||
self.assertEqual(
|
||||
update_history_mock.call_args,
|
||||
((parse_return_val,), {"send_email": True}),
|
||||
)
|
||||
|
||||
@override_settings(IANA_SYNC_PROTOCOLS_URL="https://iana.example.com/proto/")
|
||||
@mock.patch("ietf.sync.tasks.iana.update_rfc_log_from_protocol_page")
|
||||
@mock.patch("ietf.sync.tasks.iana.parse_protocol_page")
|
||||
@mock.patch("ietf.sync.tasks.requests.get")
|
||||
def test_iana_protocols_update_task(
|
||||
self,
|
||||
requests_get_mock,
|
||||
parse_protocols_mock,
|
||||
update_rfc_log_mock,
|
||||
):
|
||||
# set up mocks
|
||||
requests_get_mock.return_value = mock.Mock(text="fetched response")
|
||||
parse_protocols_mock.return_value = range(110) # larger than batch size of 100
|
||||
update_rfc_log_mock.return_value = [
|
||||
mock.Mock(display_name=mock.Mock(return_value="name"))
|
||||
]
|
||||
|
||||
# call the task
|
||||
tasks.iana_protocols_update_task()
|
||||
|
||||
# check that it did the right things
|
||||
self.assertTrue(requests_get_mock.called)
|
||||
self.assertEqual(
|
||||
requests_get_mock.call_args[0],
|
||||
("https://iana.example.com/proto/",),
|
||||
)
|
||||
self.assertTrue(parse_protocols_mock.called)
|
||||
self.assertEqual(
|
||||
parse_protocols_mock.call_args[0],
|
||||
("fetched response",),
|
||||
)
|
||||
self.assertEqual(update_rfc_log_mock.call_count, 2)
|
||||
self.assertEqual(
|
||||
update_rfc_log_mock.call_args_list[0][0][0],
|
||||
range(100), # first batch
|
||||
)
|
||||
self.assertEqual(
|
||||
update_rfc_log_mock.call_args_list[1][0][0],
|
||||
range(100, 110), # second batch
|
||||
)
|
||||
# make sure the calls use the same later_than date and that it's the expected one
|
||||
published_later_than = set(
|
||||
update_rfc_log_mock.call_args_list[n][0][1] for n in (0, 1)
|
||||
)
|
||||
self.assertEqual(
|
||||
published_later_than,
|
||||
{datetime.datetime(2012,11,26,tzinfo=datetime.timezone.utc)}
|
||||
)
|
||||
|
||||
# try with an exception
|
||||
requests_get_mock.reset_mock()
|
||||
parse_protocols_mock.reset_mock()
|
||||
update_rfc_log_mock.reset_mock()
|
||||
requests_get_mock.side_effect = requests.Timeout
|
||||
|
||||
tasks.iana_protocols_update_task()
|
||||
self.assertTrue(requests_get_mock.called)
|
||||
self.assertFalse(parse_protocols_mock.called)
|
||||
self.assertFalse(update_rfc_log_mock.called)
|
||||
|
|
|
@ -17,6 +17,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||
|
||||
from ietf.doc.models import DeletedEvent, StateDocEvent, DocEvent
|
||||
from ietf.ietfauth.utils import role_required, has_role
|
||||
from ietf.sync import tasks
|
||||
from ietf.sync.discrepancies import find_discrepancies
|
||||
from ietf.utils.serialize import object_as_shallow_dict
|
||||
from ietf.utils.log import log
|
||||
|
@ -91,19 +92,18 @@ def notify(request, org, notification):
|
|||
log("Subprocess error %s when running '%s': %s %s" % (p.returncode, cmd, err, out))
|
||||
raise subprocess.CalledProcessError(p.returncode, cmdstring, "\n".join([err, out]))
|
||||
|
||||
log("Running sync script from notify view POST")
|
||||
|
||||
if notification == "protocols":
|
||||
runscript("iana-protocols-updates")
|
||||
|
||||
if notification == "changes":
|
||||
runscript("iana-changes-updates")
|
||||
|
||||
if notification == "queue":
|
||||
runscript("rfc-editor-queue-updates")
|
||||
|
||||
if notification == "index":
|
||||
runscript("rfc-editor-index-updates")
|
||||
log("Queuing RFC Editor index sync from notify view POST")
|
||||
tasks.rfc_editor_index_update_task.delay()
|
||||
elif notification == "changes":
|
||||
log("Queuing IANA changes sync from notify view POST")
|
||||
tasks.iana_changes_update_task.delay()
|
||||
elif notification == "protocols":
|
||||
log("Queuing IANA protocols sync from notify view POST")
|
||||
tasks.iana_protocols_update_task.delay()
|
||||
elif notification == "queue":
|
||||
log("Running sync script from notify view POST")
|
||||
runscript("rfc-editor-queue-updates")
|
||||
|
||||
return HttpResponse("OK", content_type="text/plain; charset=%s"%settings.DEFAULT_CHARSET)
|
||||
|
||||
|
|
|
@ -186,12 +186,6 @@
|
|||
Sync discrepancies
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item {% if flavor != 'top' %} text-wrap{% endif %}"
|
||||
href="{% url 'ietf.ietfauth.views.add_account_allowlist' %}">
|
||||
Account allowlist
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if user|has_role:"IANA" %}
|
||||
{% if flavor == "top" %}
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2016, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% block title %}Set up test email address{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
{% if success %}
|
||||
<h1>Allowlist entry creation successful</h1>
|
||||
<p>
|
||||
Please ask the requestor to try the
|
||||
<a href="{% url 'ietf.ietfauth.views.create_account' %}">account creation form</a>
|
||||
again, with the allowlisted email address.
|
||||
</p>
|
||||
{% else %}
|
||||
<h1>Add an allowlist entry for account creation.</h1>
|
||||
<p>
|
||||
When an email request comes in for assistance with account creation
|
||||
because the automated account creation has failed, you can add the
|
||||
address to an account creation allowlist here.
|
||||
</p>
|
||||
<p>
|
||||
Before you do so, please complete the following 3 verification steps:
|
||||
</p>
|
||||
<ol class="d-grid gap-3">
|
||||
<li>
|
||||
Has the person provided relevant information in his request, or has he simply
|
||||
copied the text from the account creation failure message? All genuine (non-spam)
|
||||
account creation requests seen between 2009 and 2016 for tools.ietf.org have
|
||||
contained a reasonable request message, rather than just copy-pasting the account
|
||||
creation failure message. If there's no proper request message, step 2 below can
|
||||
be performed to make sure the request is bogus, but if that also fails, no further
|
||||
effort should be needed.
|
||||
</li>
|
||||
<li>
|
||||
Google for the person's name within the ietf.org site: "Jane Doe site:ietf.org". If
|
||||
found, and the email address matches an address used in Internet-Drafts or discussions,
|
||||
things are fine, and it's OK to add the address to the allowlist using this form,
|
||||
and ask the person to please try the
|
||||
<a href="{% url 'ietf.ietfauth.views.create_account' %}">account creation form</a>
|
||||
again.
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
If google finds no trace of the person being an ietf participant, he or she could
|
||||
still be somebody who is just getting involved in IETF work. A datatracker account
|
||||
is probably not necessary, (no account is necessary to 'join' a WG -- the right thing
|
||||
in that case is to join the right mailing list, and the person could be told so) --
|
||||
but in case this is a legitimate request, please email the person and ask:
|
||||
<i>
|
||||
"Which wgs do you require a password for?"
|
||||
</i>
|
||||
</p>
|
||||
<p>
|
||||
This is a bit of a trick question, because it is very unlikely that somebody who
|
||||
isn't involved in IETF work will give a reasonable response, while almost any answer
|
||||
from somebody who is doing IETF work will show that they have some clue.
|
||||
</p>
|
||||
<p>
|
||||
Please note the exact wording. Do <b><i>not</i></b> ask about "working groups" --
|
||||
that will make it easier for people to google for IETF working groups. Ask the
|
||||
question as given above, with lowercase "wgs".
|
||||
</p>
|
||||
<p>
|
||||
If the answer to this question shows clue, then add the address to the allowlist
|
||||
using this form, and ask the person to please try the
|
||||
<a href="{% url 'ietf.ietfauth.views.create_account' %}">account creation form</a>
|
||||
again.
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button class="btn btn-primary" type="submit">Add address to account creation allowlist</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -5,32 +5,41 @@ from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
CRONTAB_DEFS = {
|
||||
# same as "@weekly" in a crontab
|
||||
"weekly": {
|
||||
"minute": "0",
|
||||
"hour": "0",
|
||||
"day_of_month": "*",
|
||||
"month_of_year": "*",
|
||||
"day_of_week": "0",
|
||||
},
|
||||
"daily": {
|
||||
"minute": "5",
|
||||
"hour": "0",
|
||||
"day_of_week": "*",
|
||||
"day_of_month": "*",
|
||||
"month_of_year": "*",
|
||||
"day_of_week": "*",
|
||||
},
|
||||
"hourly": {
|
||||
"minute": "5",
|
||||
"hour": "*",
|
||||
"day_of_week": "*",
|
||||
"day_of_month": "*",
|
||||
"month_of_year": "*",
|
||||
"day_of_week": "*",
|
||||
},
|
||||
"every_15m": {
|
||||
"minute": "*/15",
|
||||
"hour": "*",
|
||||
"day_of_week": "*",
|
||||
"day_of_month": "*",
|
||||
"month_of_year": "*",
|
||||
"day_of_week": "*",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Manage periodic tasks"""
|
||||
crontabs = None
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--create-default", action="store_true")
|
||||
|
@ -112,6 +121,56 @@ class Command(BaseCommand):
|
|||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Expire I-Ds",
|
||||
task="ietf.doc.tasks.expire_ids_task",
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["daily"],
|
||||
description="Create expiration notices for expired I-Ds",
|
||||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Sync with IANA changes",
|
||||
task="ietf.sync.tasks.iana_changes_update_task",
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["hourly"],
|
||||
description="Fetch change list from IANA and apply to documents",
|
||||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Sync with IANA protocols page",
|
||||
task="ietf.sync.tasks.iana_changes_update_task",
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["hourly"],
|
||||
description="Fetch protocols page from IANA and update document event logs",
|
||||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Update I-D index files",
|
||||
task="ietf.idindex.tasks.idindex_update_task",
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["hourly"],
|
||||
description="Update I-D index files",
|
||||
),
|
||||
)
|
||||
|
||||
PeriodicTask.objects.get_or_create(
|
||||
name="Send expiration notifications",
|
||||
task="ietf.doc.tasks.notify_expirations_task",
|
||||
defaults=dict(
|
||||
enabled=False,
|
||||
crontab=self.crontabs["weekly"],
|
||||
description="Send notifications about I-Ds that will expire in the next 14 days",
|
||||
)
|
||||
)
|
||||
|
||||
def show_tasks(self):
|
||||
for label, crontab in self.crontabs.items():
|
||||
tasks = PeriodicTask.objects.filter(crontab=crontab).order_by(
|
||||
|
|
|
@ -84,7 +84,7 @@ def make_immutable_base_data():
|
|||
create_person(iab, "chair")
|
||||
create_person(iab, "member")
|
||||
|
||||
ise = create_group(name="Independent Submission Editor", acronym="ise", type_id="rfcedtyp")
|
||||
ise = create_group(name="Independent Submission Editor", acronym="ise", type_id="ise")
|
||||
create_person(ise, "chair")
|
||||
|
||||
rsoc = create_group(name="RFC Series Oversight Committee", acronym="rsoc", type_id="rfcedtyp")
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"bootstrap": "5.3.2",
|
||||
"bootstrap-icons": "1.11.3",
|
||||
"browser-fs-access": "0.35.0",
|
||||
"caniuse-lite": "1.0.30001576",
|
||||
"caniuse-lite": "1.0.30001581",
|
||||
"d3": "7.8.5",
|
||||
"file-saver": "2.0.5",
|
||||
"highcharts": "11.3.0",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"slugify": "1.6.6",
|
||||
"sortablejs": "1.15.2",
|
||||
"vanillajs-datepicker": "1.3.4",
|
||||
"vue": "3.4.13",
|
||||
"vue": "3.4.15",
|
||||
"vue-router": "4.2.5",
|
||||
"zxcvbn": "4.4.2"
|
||||
},
|
||||
|
@ -70,9 +70,9 @@
|
|||
"jquery-migrate": "3.4.1",
|
||||
"parcel": "2.11.0",
|
||||
"pug": "3.0.2",
|
||||
"sass": "1.69.7",
|
||||
"sass": "1.70.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"vite": "4.5.1"
|
||||
"vite": "4.5.2"
|
||||
},
|
||||
"targets": {
|
||||
"ietf": {
|
||||
|
|
74
playwright/package-lock.json
generated
74
playwright/package-lock.json
generated
|
@ -6,7 +6,7 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "8.3.1",
|
||||
"@faker-js/faker": "8.4.0",
|
||||
"lodash": "4.17.21",
|
||||
"lodash-es": "4.17.21",
|
||||
"luxon": "3.4.4",
|
||||
|
@ -15,14 +15,14 @@
|
|||
"slugify": "1.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.40.1",
|
||||
"@playwright/test": "1.41.2",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-n": "16.6.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"npm-check-updates": "16.14.12"
|
||||
"npm-check-updates": "16.14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
@ -101,9 +101,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.3.1.tgz",
|
||||
"integrity": "sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw==",
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.0.tgz",
|
||||
"integrity": "sha512-htW87352wzUCdX1jyUQocUcmAaFqcR/w082EC8iP/gtkF0K+aKcBp0hR5Arb7dzR8tQ1TrhE9DNa5EbJELm84w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
@ -399,12 +399,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz",
|
||||
"integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==",
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
|
||||
"integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.40.1"
|
||||
"playwright": "1.41.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
@ -3804,9 +3804,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/npm-check-updates": {
|
||||
"version": "16.14.12",
|
||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.12.tgz",
|
||||
"integrity": "sha512-5FvqaDX8AqWWTDQFbBllgLwoRXTvzlqVIRSKl9Kg8bYZTfNwMnrp1Zlmb5e/ocf11UjPTc+ShBFjYQ7kg6FL0w==",
|
||||
"version": "16.14.14",
|
||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.14.tgz",
|
||||
"integrity": "sha512-Y3ajS/Ep40jM489rLBdz9jehn/BMil5s9fA4PSr2ZJxxSmtLWCSmRqsI2IEZ9Nb3MTMu8a3s7kBs0l+JbjdkTA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
|
@ -4407,12 +4407,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.40.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz",
|
||||
"integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==",
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
|
||||
"integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.40.1"
|
||||
"playwright-core": "1.41.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
@ -4425,9 +4425,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.40.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz",
|
||||
"integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==",
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
|
||||
"integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
@ -6015,9 +6015,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@faker-js/faker": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.3.1.tgz",
|
||||
"integrity": "sha512-FdgpFxY6V6rLZE9mmIBb9hM0xpfvQOSNOLnzolzKwsE1DH+gC7lEKV1p1IbR0lAYyvYd5a4u3qWJzowUkw1bIw=="
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.0.tgz",
|
||||
"integrity": "sha512-htW87352wzUCdX1jyUQocUcmAaFqcR/w082EC8iP/gtkF0K+aKcBp0hR5Arb7dzR8tQ1TrhE9DNa5EbJELm84w=="
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.11.13",
|
||||
|
@ -6226,12 +6226,12 @@
|
|||
"optional": true
|
||||
},
|
||||
"@playwright/test": {
|
||||
"version": "1.40.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz",
|
||||
"integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==",
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
|
||||
"integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"playwright": "1.40.1"
|
||||
"playwright": "1.41.2"
|
||||
}
|
||||
},
|
||||
"@pnpm/network.ca-file": {
|
||||
|
@ -8721,9 +8721,9 @@
|
|||
}
|
||||
},
|
||||
"npm-check-updates": {
|
||||
"version": "16.14.12",
|
||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.12.tgz",
|
||||
"integrity": "sha512-5FvqaDX8AqWWTDQFbBllgLwoRXTvzlqVIRSKl9Kg8bYZTfNwMnrp1Zlmb5e/ocf11UjPTc+ShBFjYQ7kg6FL0w==",
|
||||
"version": "16.14.14",
|
||||
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.14.tgz",
|
||||
"integrity": "sha512-Y3ajS/Ep40jM489rLBdz9jehn/BMil5s9fA4PSr2ZJxxSmtLWCSmRqsI2IEZ9Nb3MTMu8a3s7kBs0l+JbjdkTA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^5.3.0",
|
||||
|
@ -9149,19 +9149,19 @@
|
|||
"dev": true
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.40.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz",
|
||||
"integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==",
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz",
|
||||
"integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.40.1"
|
||||
"playwright-core": "1.41.2"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.40.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz",
|
||||
"integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==",
|
||||
"version": "1.41.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz",
|
||||
"integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==",
|
||||
"dev": true
|
||||
},
|
||||
"prelude-ls": {
|
||||
|
|
|
@ -7,17 +7,17 @@
|
|||
"test:debug": "playwright test --debug"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.40.1",
|
||||
"@playwright/test": "1.41.2",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-n": "16.6.2",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"npm-check-updates": "16.14.12"
|
||||
"npm-check-updates": "16.14.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "8.3.1",
|
||||
"@faker-js/faker": "8.4.0",
|
||||
"lodash": "4.17.21",
|
||||
"lodash-es": "4.17.21",
|
||||
"luxon": "3.4.4",
|
||||
|
|
171
yarn.lock
171
yarn.lock
|
@ -2018,53 +2018,53 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/compiler-core@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/compiler-core@npm:3.4.13"
|
||||
"@vue/compiler-core@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/compiler-core@npm:3.4.15"
|
||||
dependencies:
|
||||
"@babel/parser": ^7.23.6
|
||||
"@vue/shared": 3.4.13
|
||||
"@vue/shared": 3.4.15
|
||||
entities: ^4.5.0
|
||||
estree-walker: ^2.0.2
|
||||
source-map-js: ^1.0.2
|
||||
checksum: 5f486b5ca816db693f9cee44a8855f4de0bd83de2f423c10c800a19bcf2e864b74e04b75542948cd20baea4824e9c2eec2e492357a0cfe7f1a954177a9442b79
|
||||
checksum: 1610f715b8ab6de95aa9f904d484ed275cf39e947d3fbb92a8ff7d7178360b71cfeae2710ef819dbeb738e1f94bf191298449719a2ecc860389338bcdef220f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/compiler-dom@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/compiler-dom@npm:3.4.13"
|
||||
"@vue/compiler-dom@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/compiler-dom@npm:3.4.15"
|
||||
dependencies:
|
||||
"@vue/compiler-core": 3.4.13
|
||||
"@vue/shared": 3.4.13
|
||||
checksum: 2afdacc03835425bd29a841a4d3a64bf0a60a53d73fc596933ce40e3577c45a7e06edc6f79207890b96a10f4f6bfd74e43ec4807253497fe55cf60db7e12204c
|
||||
"@vue/compiler-core": 3.4.15
|
||||
"@vue/shared": 3.4.15
|
||||
checksum: 373968c2c603f4eb9ebbf5f31ca2dc89991c4c1b0cee0213e613ad8b4ee632a33174e92bd91e0f8ff65f55188b46b742b91269a098c1e421d8f8bc919d5adc25
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/compiler-sfc@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/compiler-sfc@npm:3.4.13"
|
||||
"@vue/compiler-sfc@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/compiler-sfc@npm:3.4.15"
|
||||
dependencies:
|
||||
"@babel/parser": ^7.23.6
|
||||
"@vue/compiler-core": 3.4.13
|
||||
"@vue/compiler-dom": 3.4.13
|
||||
"@vue/compiler-ssr": 3.4.13
|
||||
"@vue/shared": 3.4.13
|
||||
"@vue/compiler-core": 3.4.15
|
||||
"@vue/compiler-dom": 3.4.15
|
||||
"@vue/compiler-ssr": 3.4.15
|
||||
"@vue/shared": 3.4.15
|
||||
estree-walker: ^2.0.2
|
||||
magic-string: ^0.30.5
|
||||
postcss: ^8.4.32
|
||||
postcss: ^8.4.33
|
||||
source-map-js: ^1.0.2
|
||||
checksum: 9252b9f10c9f0d730afbd2a2963fbbd2784ffdfa0e9a35c3e0366c5081423c7cb091c35f663ee43d587ade2dea8ed4d0329db76b76d9dd5546c457a7ac65f95d
|
||||
checksum: 4a707346c32b6deaec47c4bb1fddaaa6ec881e286db59de8922960f52a617ff7bebfcbe19e80c98a0fd91d0f575d962787f77c16ac10a7eaac7d938c48bfb4c7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/compiler-ssr@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/compiler-ssr@npm:3.4.13"
|
||||
"@vue/compiler-ssr@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/compiler-ssr@npm:3.4.15"
|
||||
dependencies:
|
||||
"@vue/compiler-dom": 3.4.13
|
||||
"@vue/shared": 3.4.13
|
||||
checksum: 99fae88e1312b138888e7df90064448a17f368d6e640f726f50233d261eb50e789cee83bc891b09015ea2a5fe0939db0b2c54c9b790e296991f5c420ebab1c20
|
||||
"@vue/compiler-dom": 3.4.15
|
||||
"@vue/shared": 3.4.15
|
||||
checksum: 45a12ae2dd2e645db53d43b3c27df1d8fbf0584199d6e5581c96b4566d889376f5da411f8e453e113e3dcae0f2cc80b6f6fb36110f3f42f5cc260e48a99dd37f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -2075,52 +2075,52 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/reactivity@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/reactivity@npm:3.4.13"
|
||||
"@vue/reactivity@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/reactivity@npm:3.4.15"
|
||||
dependencies:
|
||||
"@vue/shared": 3.4.13
|
||||
checksum: 883ba2fb31ce9366d51f686c793ebab4374610acb903706d6de095d737079692a6b87b6973b4170af2f363dd82c0d507f41ca49ec345f6b74665d152f4b8b0c8
|
||||
"@vue/shared": 3.4.15
|
||||
checksum: e1f8ef7ec3e933b5dd5e3aa3e281c38d1fd2834772016ea5193058d80342704afbed0e7728cf31eb5762c2705785eec98b3d154ae22005691bee5b35125a4d7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/runtime-core@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/runtime-core@npm:3.4.13"
|
||||
"@vue/runtime-core@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/runtime-core@npm:3.4.15"
|
||||
dependencies:
|
||||
"@vue/reactivity": 3.4.13
|
||||
"@vue/shared": 3.4.13
|
||||
checksum: 196c6c894d416c4a05d3811ff790d1bcc909220007a4aa3aafe03f85bf9d8e8c14dc9dbb063bccee2b4803c8581e50359fc1417e4e786d481e2cfd26f8299358
|
||||
"@vue/reactivity": 3.4.15
|
||||
"@vue/shared": 3.4.15
|
||||
checksum: 6ab6721410ce5379d3a0de8632527be5cae26adda33854bd32117cf395713d41980f47b3774ba4dfbe7242377397d61a5728aa14b6a0fbd9e8f77049ef1ca4a4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/runtime-dom@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/runtime-dom@npm:3.4.13"
|
||||
"@vue/runtime-dom@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/runtime-dom@npm:3.4.15"
|
||||
dependencies:
|
||||
"@vue/runtime-core": 3.4.13
|
||||
"@vue/shared": 3.4.13
|
||||
"@vue/runtime-core": 3.4.15
|
||||
"@vue/shared": 3.4.15
|
||||
csstype: ^3.1.3
|
||||
checksum: 8811687c23e9f31e87bff8d97f9a20a9d78fe45b66f724fe4bcb2aa669a67328df615aa3bf5ea02a2e22a0c5459bab278e01b5fae31dc22c5e09e765df867bce
|
||||
checksum: 4f2e79d95688dc110629d4879ce6cc9bdaf284a29636c28ea9bc5cb420649eaac7d1a545e11d54516311b0cfdc507a2979aaaf89e9eddd386d41ee36d29db60e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/server-renderer@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/server-renderer@npm:3.4.13"
|
||||
"@vue/server-renderer@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/server-renderer@npm:3.4.15"
|
||||
dependencies:
|
||||
"@vue/compiler-ssr": 3.4.13
|
||||
"@vue/shared": 3.4.13
|
||||
"@vue/compiler-ssr": 3.4.15
|
||||
"@vue/shared": 3.4.15
|
||||
peerDependencies:
|
||||
vue: 3.4.13
|
||||
checksum: f17fff6af28f50bc552b5c798cb5ca595651863a52e62e4ed8b53448df870d2311e78ca1d513cf721168c3b17edd66700ccb7fe280372c84d0d8015787a786ee
|
||||
vue: 3.4.15
|
||||
checksum: de93ccffe7008a12974d6f82024238f7b7b25817aae6846dabdcfb8534a6ce01528f7b13447b2561394112e4b6fd1bd125c3391c0ac9d849c6de167bf44f4e55
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/shared@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "@vue/shared@npm:3.4.13"
|
||||
checksum: c514944886d08d85bde55dc4a116ac4c295f5fc003fd70f03bcb64e074c7367703611916cf05101c304c8df2ae91d0f9cddfd54175b94b070d02a90ff07d0411
|
||||
"@vue/shared@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "@vue/shared@npm:3.4.15"
|
||||
checksum: 237db3a880692c69358c46679562cee85d8495090a3c8ed44a4d4daa7c4a61d74e330b9bd1f3cec7362a2ae443f46186be8a86b44bff7604d5bd72ad994b8021
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -2629,10 +2629,10 @@ browserlist@latest:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"caniuse-lite@npm:1.0.30001576":
|
||||
version: 1.0.30001576
|
||||
resolution: "caniuse-lite@npm:1.0.30001576"
|
||||
checksum: b8b332675fe703d5e57b02df5f100345f2a3796c537a42422f5bfc82d3256b8bad3f4e2788553656d2650006d13a4b5db99725e2a9462cc0c8035ba494ba1857
|
||||
"caniuse-lite@npm:1.0.30001581":
|
||||
version: 1.0.30001581
|
||||
resolution: "caniuse-lite@npm:1.0.30001581"
|
||||
checksum: ca4e2cd9d0acf5e3c71fa2e7cd65561e4532d32b640145f634c333792074bb63de1239b35abfb6b6d372f97caf26f8d97faac7ba51ef190717ad2d3ae9c0d7a2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -6488,7 +6488,7 @@ browserlist@latest:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:^8.4.27":
|
||||
"postcss@npm:^8.4.27, postcss@npm:^8.4.33":
|
||||
version: 8.4.33
|
||||
resolution: "postcss@npm:8.4.33"
|
||||
dependencies:
|
||||
|
@ -6499,17 +6499,6 @@ browserlist@latest:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:^8.4.32":
|
||||
version: 8.4.32
|
||||
resolution: "postcss@npm:8.4.32"
|
||||
dependencies:
|
||||
nanoid: ^3.3.7
|
||||
picocolors: ^1.0.0
|
||||
source-map-js: ^1.0.2
|
||||
checksum: 220d9d0bf5d65be7ed31006c523bfb11619461d296245c1231831f90150aeb4a31eab9983ac9c5c89759a3ca8b60b3e0d098574964e1691673c3ce5c494305ae
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"posthtml-parser@npm:^0.10.1":
|
||||
version: 0.10.2
|
||||
resolution: "posthtml-parser@npm:0.10.2"
|
||||
|
@ -6988,7 +6977,7 @@ browserlist@latest:
|
|||
browser-fs-access: 0.35.0
|
||||
browserlist: latest
|
||||
c8: 9.1.0
|
||||
caniuse-lite: 1.0.30001576
|
||||
caniuse-lite: 1.0.30001581
|
||||
d3: 7.8.5
|
||||
eslint: 8.56.0
|
||||
eslint-config-standard: 17.1.0
|
||||
|
@ -7018,7 +7007,7 @@ browserlist@latest:
|
|||
pinia: 2.1.7
|
||||
pinia-plugin-persist: 1.0.0
|
||||
pug: 3.0.2
|
||||
sass: 1.69.7
|
||||
sass: 1.70.0
|
||||
seedrandom: 3.0.5
|
||||
select2: 4.1.0-rc.0
|
||||
select2-bootstrap-5-theme: 1.3.0
|
||||
|
@ -7027,8 +7016,8 @@ browserlist@latest:
|
|||
slugify: 1.6.6
|
||||
sortablejs: 1.15.2
|
||||
vanillajs-datepicker: 1.3.4
|
||||
vite: 4.5.1
|
||||
vue: 3.4.13
|
||||
vite: 4.5.2
|
||||
vue: 3.4.15
|
||||
vue-router: 4.2.5
|
||||
zxcvbn: 4.4.2
|
||||
languageName: unknown
|
||||
|
@ -7094,16 +7083,16 @@ browserlist@latest:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sass@npm:1.69.7":
|
||||
version: 1.69.7
|
||||
resolution: "sass@npm:1.69.7"
|
||||
"sass@npm:1.70.0":
|
||||
version: 1.70.0
|
||||
resolution: "sass@npm:1.70.0"
|
||||
dependencies:
|
||||
chokidar: ">=3.0.0 <4.0.0"
|
||||
immutable: ^4.0.0
|
||||
source-map-js: ">=0.6.2 <2.0.0"
|
||||
bin:
|
||||
sass: sass.js
|
||||
checksum: c67cd32b69fb26a50e4535353e4145de8cbc8187db07c467cc335157fd56d03cae98754f86efe43b880b29f20c0a168ab972c7f74ebfe234e2bd2dfb868890cb
|
||||
checksum: fd1b622cf9b7fa699a03ec634611997552ece45eb98ac365fef22f42bdcb8ed63b326b64173379c966830c8551ae801e44e4a00d2de16fdadda2dc8f35400bbb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7811,9 +7800,9 @@ browserlist@latest:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:4.5.1":
|
||||
version: 4.5.1
|
||||
resolution: "vite@npm:4.5.1"
|
||||
"vite@npm:4.5.2":
|
||||
version: 4.5.2
|
||||
resolution: "vite@npm:4.5.2"
|
||||
dependencies:
|
||||
esbuild: ^0.18.10
|
||||
fsevents: ~2.3.2
|
||||
|
@ -7847,7 +7836,7 @@ browserlist@latest:
|
|||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: 72b3584b3d3b8d14e8a37f0248e47fb8b4d02ab35de5b5a8e5ca8ae55c3be2aab73760dc36edac4fa722de182f78cc492eb44888fcb4a9a0712c4605dad644f9
|
||||
checksum: 9d1f84f703c2660aced34deee7f309278ed368880f66e9570ac115c793d91f7fffb80ab19c602b3c8bc1341fe23437d86a3fcca2a9ef82f7ef0cdac5a40d0c86
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -7929,21 +7918,21 @@ browserlist@latest:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vue@npm:3.4.13":
|
||||
version: 3.4.13
|
||||
resolution: "vue@npm:3.4.13"
|
||||
"vue@npm:3.4.15":
|
||||
version: 3.4.15
|
||||
resolution: "vue@npm:3.4.15"
|
||||
dependencies:
|
||||
"@vue/compiler-dom": 3.4.13
|
||||
"@vue/compiler-sfc": 3.4.13
|
||||
"@vue/runtime-dom": 3.4.13
|
||||
"@vue/server-renderer": 3.4.13
|
||||
"@vue/shared": 3.4.13
|
||||
"@vue/compiler-dom": 3.4.15
|
||||
"@vue/compiler-sfc": 3.4.15
|
||||
"@vue/runtime-dom": 3.4.15
|
||||
"@vue/server-renderer": 3.4.15
|
||||
"@vue/shared": 3.4.15
|
||||
peerDependencies:
|
||||
typescript: "*"
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: c9f8edf5fc8bcab2254a8b4cbcb9c6fa6c0f588521ecf98b8a315da1e87e817c50a2ab2d2f0339518bf9cbe252a558a44b36bef25825c11d8f9b1e214608b6c0
|
||||
checksum: 6e9ff02c9bd46cb47ff2225e7b51b75b00343b7f52076a56c2a90ce15de88c1de1aaa6b176ac39ca324479ee208b7f7e7992f54a353b0ee6b303081ac5ab30b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Loading…
Reference in a new issue