ci: merge main to release (pull request #7034 from ietf-tools/main)

ci: merge main to release
This commit is contained in:
Robert Sparks 2024-02-07 13:49:58 -06:00 committed by GitHub
commit ac4062f921
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 2741 additions and 749 deletions

View file

@ -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,10 +196,46 @@ 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
@ -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: |

View file

@ -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

View file

@ -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
View file

@ -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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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
View 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"]

View 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
View 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

View file

@ -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",

View file

@ -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"
}
}

View file

@ -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

View file

@ -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
View 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
View 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"

View 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 }}

View 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
View 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 }}

View 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 }}

View 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}}

View 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
View 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: {}

View file

@ -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

View file

@ -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',
@ -780,7 +784,74 @@ class CustomApiTests(TestCase):
url = urlreverse('ietf.meeting.views.api_get_session_materials', kwargs={'session_id': session.pk})
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"),
)

View file

@ -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

View file

@ -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)

View 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,
)

View file

@ -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()

View file

@ -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
View 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)

View file

@ -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
View 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",))

View file

@ -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)

View file

@ -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):

View file

@ -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
View 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")

View file

@ -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)

View file

@ -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"

View file

@ -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),
]

View file

@ -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")

View file

@ -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):

View file

@ -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))

View file

@ -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)

View 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),
]

View file

@ -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",
),
]

View file

@ -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])

View file

@ -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())

View file

@ -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)

View file

@ -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})

View file

@ -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 %}

View file

@ -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.

View file

@ -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())

View file

@ -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)

View file

@ -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)

View file

@ -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" %}

View file

@ -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 %}

View file

@ -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(

View file

@ -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")

View file

@ -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": {

View file

@ -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": {

View file

@ -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
View file

@ -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