ci: merge main to release (pull request #7265)

ci: merge main to release
This commit is contained in:
Robert Sparks 2024-03-28 08:58:23 -05:00 committed by GitHub
commit c41c0bbad9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 868 additions and 2115 deletions

View file

@ -201,8 +201,8 @@ jobs:
- name: Collect + Push Statics
env:
DEBIAN_FRONTEND: noninteractive
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_KEY_SECRET }}
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_STATIC_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_STATIC_KEY_SECRET }}
AWS_DEFAULT_REGION: auto
AWS_ENDPOINT_URL: ${{ secrets.CF_R2_ENDPOINT }}
run: |

1
.gitignore vendored
View file

@ -26,6 +26,7 @@ datatracker.sublime-workspace
/static
/tmp-*
/.testresult
*.swp
*.pyc
__pycache__
.yarn/*

244
.pnp.cjs generated
View file

@ -49,13 +49,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@rollup/pluginutils", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.1.0"],\
["@twuni/emojify", "npm:1.0.2"],\
["@vitejs/plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.6.2"],\
["@vue/language-plugin-pug", "npm:2.0.7"],\
["bootstrap", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.3.3"],\
["bootstrap-icons", "npm:1.11.3"],\
["browser-fs-access", "npm:0.35.0"],\
["browserlist", "npm:1.0.1"],\
["c8", "npm:9.1.0"],\
["caniuse-lite", "npm:1.0.30001597"],\
["d3", "npm:7.8.5"],\
["caniuse-lite", "npm:1.0.30001599"],\
["d3", "npm:7.9.0"],\
["eslint", "npm:8.57.0"],\
["eslint-config-standard", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:17.1.0"],\
["eslint-plugin-cypress", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.15.1"],\
@ -63,10 +64,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["eslint-plugin-n", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:16.6.2"],\
["eslint-plugin-node", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:11.1.0"],\
["eslint-plugin-promise", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.1"],\
["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.22.0"],\
["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.23.0"],\
["file-saver", "npm:2.0.5"],\
["highcharts", "npm:11.4.0"],\
["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.15.0"],\
["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.16.0"],\
["ical.js", "npm:1.5.0"],\
["jquery", "npm:3.7.1"],\
["jquery-migrate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.1"],\
@ -84,7 +85,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.71.1"],\
["sass", "npm:1.72.0"],\
["seedrandom", "npm:3.0.5"],\
["select2", "npm:4.1.0-rc.0"],\
["select2-bootstrap-5-theme", "npm:1.3.0"],\
@ -2431,10 +2432,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "SOFT"\
}],\
["virtual:2a2a921469e6f0bfdb6b2bd79f75a3395d47a481854507365048f3d989418f207cf814cb2ce1a012d2da774c1d130b4ca418582463ec08381da55e543b959c4c#npm:2.1.3", {\
"packageLocation": "./.yarn/__virtual__/@sidvind-better-ajv-errors-virtual-6ac4a81dfc/0/cache/@sidvind-better-ajv-errors-npm-2.1.3-e3d1c524a8-949cb805a1.zip/node_modules/@sidvind/better-ajv-errors/",\
["virtual:127f44ed2d4bdd83725c8820683da6ed922142e851761151ec4911dcca098a2d4d832b774a31e7abf98185d360ff83c999b319ea3314c86d38eca853072fb0d2#npm:2.1.3", {\
"packageLocation": "./.yarn/__virtual__/@sidvind-better-ajv-errors-virtual-ae7b5eb579/0/cache/@sidvind-better-ajv-errors-npm-2.1.3-e3d1c524a8-949cb805a1.zip/node_modules/@sidvind/better-ajv-errors/",\
"packageDependencies": [\
["@sidvind/better-ajv-errors", "virtual:2a2a921469e6f0bfdb6b2bd79f75a3395d47a481854507365048f3d989418f207cf814cb2ce1a012d2da774c1d130b4ca418582463ec08381da55e543b959c4c#npm:2.1.3"],\
["@sidvind/better-ajv-errors", "virtual:127f44ed2d4bdd83725c8820683da6ed922142e851761151ec4911dcca098a2d4d832b774a31e7abf98185d360ff83c999b319ea3314c86d38eca853072fb0d2#npm:2.1.3"],\
["@babel/code-frame", "npm:7.16.7"],\
["@types/ajv", null],\
["ajv", "npm:8.11.0"],\
@ -2721,6 +2722,48 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
["@volar/language-core", [\
["npm:2.1.4", {\
"packageLocation": "./.yarn/cache/@volar-language-core-npm-2.1.4-18ee1a037d-7430f65143.zip/node_modules/@volar/language-core/",\
"packageDependencies": [\
["@volar/language-core", "npm:2.1.4"],\
["@volar/source-map", "npm:2.1.4"]\
],\
"linkType": "HARD"\
}]\
]],\
["@volar/language-service", [\
["npm:2.1.4", {\
"packageLocation": "./.yarn/cache/@volar-language-service-npm-2.1.4-2d34cb628f-06cdcfacf0.zip/node_modules/@volar/language-service/",\
"packageDependencies": [\
["@volar/language-service", "npm:2.1.4"],\
["@volar/language-core", "npm:2.1.4"],\
["vscode-languageserver-protocol", "npm:3.17.5"],\
["vscode-languageserver-textdocument", "npm:1.0.11"],\
["vscode-uri", "npm:3.0.8"]\
],\
"linkType": "HARD"\
}]\
]],\
["@volar/source-map", [\
["npm:2.1.4", {\
"packageLocation": "./.yarn/cache/@volar-source-map-npm-2.1.4-5963b1701f-e2f65bcfd6.zip/node_modules/@volar/source-map/",\
"packageDependencies": [\
["@volar/source-map", "npm:2.1.4"],\
["muggle-string", "npm:0.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["@vscode/l10n", [\
["npm:0.0.18", {\
"packageLocation": "./.yarn/cache/@vscode-l10n-npm-0.0.18-8a12efe4b5-c33876cebd.zip/node_modules/@vscode/l10n/",\
"packageDependencies": [\
["@vscode/l10n", "npm:0.0.18"]\
],\
"linkType": "HARD"\
}]\
]],\
["@vue/compiler-core", [\
["npm:3.4.21", {\
"packageLocation": "./.yarn/cache/@vue-compiler-core-npm-3.4.21-ec7f24d7f5-0d6b7732bc.zip/node_modules/@vue/compiler-core/",\
@ -2791,6 +2834,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
["@vue/language-plugin-pug", [\
["npm:2.0.7", {\
"packageLocation": "./.yarn/cache/@vue-language-plugin-pug-npm-2.0.7-547300c7e0-11cc96eb5f.zip/node_modules/@vue/language-plugin-pug/",\
"packageDependencies": [\
["@vue/language-plugin-pug", "npm:2.0.7"],\
["@volar/source-map", "npm:2.1.4"],\
["volar-service-pug", "npm:0.0.34"]\
],\
"linkType": "HARD"\
}]\
]],\
["@vue/reactivity", [\
["npm:3.4.21", {\
"packageLocation": "./.yarn/cache/@vue-reactivity-npm-3.4.21-fd3e254d08-79c7ebe3ec.zip/node_modules/@vue/reactivity/",\
@ -3452,10 +3506,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "HARD"\
}],\
["npm:1.0.30001597", {\
"packageLocation": "./.yarn/cache/caniuse-lite-npm-1.0.30001597-1e349680d5-ec6a2cf0fd.zip/node_modules/caniuse-lite/",\
["npm:1.0.30001599", {\
"packageLocation": "./.yarn/cache/caniuse-lite-npm-1.0.30001599-834cd4cb82-d7e619e2e7.zip/node_modules/caniuse-lite/",\
"packageDependencies": [\
["caniuse-lite", "npm:1.0.30001597"]\
["caniuse-lite", "npm:1.0.30001599"]\
],\
"linkType": "HARD"\
}]\
@ -3764,10 +3818,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["d3", [\
["npm:7.8.5", {\
"packageLocation": "./.yarn/cache/d3-npm-7.8.5-5db20a5616-e407e79731.zip/node_modules/d3/",\
["npm:7.9.0", {\
"packageLocation": "./.yarn/cache/d3-npm-7.9.0-d293821ce6-1c0e9135f1.zip/node_modules/d3/",\
"packageDependencies": [\
["d3", "npm:7.8.5"],\
["d3", "npm:7.9.0"],\
["d3-array", "npm:3.1.6"],\
["d3-axis", "npm:3.0.0"],\
["d3-brush", "npm:3.0.0"],\
@ -5041,17 +5095,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["eslint-plugin-vue", [\
["npm:9.22.0", {\
"packageLocation": "./.yarn/cache/eslint-plugin-vue-npm-9.22.0-2cdb92f3c1-5f1e94b412.zip/node_modules/eslint-plugin-vue/",\
["npm:9.23.0", {\
"packageLocation": "./.yarn/cache/eslint-plugin-vue-npm-9.23.0-d36e581933-acb3a4dd27.zip/node_modules/eslint-plugin-vue/",\
"packageDependencies": [\
["eslint-plugin-vue", "npm:9.22.0"]\
["eslint-plugin-vue", "npm:9.23.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.22.0", {\
"packageLocation": "./.yarn/__virtual__/eslint-plugin-vue-virtual-2da7be1523/0/cache/eslint-plugin-vue-npm-9.22.0-2cdb92f3c1-5f1e94b412.zip/node_modules/eslint-plugin-vue/",\
["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.23.0", {\
"packageLocation": "./.yarn/__virtual__/eslint-plugin-vue-virtual-a9cf4d7c43/0/cache/eslint-plugin-vue-npm-9.23.0-d36e581933-acb3a4dd27.zip/node_modules/eslint-plugin-vue/",\
"packageDependencies": [\
["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.22.0"],\
["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.23.0"],\
["@eslint-community/eslint-utils", "virtual:4286e12a3a0f74af013bc8f16c6d8fdde823cfbf6389660266b171e551f576c805b0a7a8eb2a7087a5cee7dfe6ebb6e1ea3808d93daf915edc95656907a381bb#npm:4.4.0"],\
["@types/eslint", null],\
["eslint", "npm:8.57.0"],\
@ -5059,7 +5113,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["nth-check", "npm:2.1.1"],\
["postcss-selector-parser", "npm:6.0.15"],\
["semver", "npm:7.6.0"],\
["vue-eslint-parser", "virtual:2da7be1523058e3d6960a64ca4697b767ffeb995ed69da9eb74f6f820005e744683aa6cde73ca784c556dfc97ad5b1bc884e0f3f37187c87e646cf74b01fce0e#npm:9.4.2"],\
["vue-eslint-parser", "virtual:a9cf4d7c437b03eeeb49b01b4b3434ca26b6f59f98831796df73ffd0744dcbe1bfe9c60f8b7e2c803ab0f29084730afdc0220af1db5d8d3d00531e0df4f49dbe#npm:9.4.2"],\
["xml-name-validator", "npm:4.0.0"]\
],\
"packagePeers": [\
@ -5774,20 +5828,20 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
}]\
]],\
["html-validate", [\
["npm:8.15.0", {\
"packageLocation": "./.yarn/cache/html-validate-npm-8.15.0-a1dfa4198d-0af7685ca1.zip/node_modules/html-validate/",\
["npm:8.16.0", {\
"packageLocation": "./.yarn/cache/html-validate-npm-8.16.0-71c478330f-857b05ab87.zip/node_modules/html-validate/",\
"packageDependencies": [\
["html-validate", "npm:8.15.0"]\
["html-validate", "npm:8.16.0"]\
],\
"linkType": "SOFT"\
}],\
["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.15.0", {\
"packageLocation": "./.yarn/__virtual__/html-validate-virtual-2a2a921469/0/cache/html-validate-npm-8.15.0-a1dfa4198d-0af7685ca1.zip/node_modules/html-validate/",\
["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.16.0", {\
"packageLocation": "./.yarn/__virtual__/html-validate-virtual-127f44ed2d/0/cache/html-validate-npm-8.16.0-71c478330f-857b05ab87.zip/node_modules/html-validate/",\
"packageDependencies": [\
["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.15.0"],\
["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.16.0"],\
["@babel/code-frame", "npm:7.16.7"],\
["@html-validate/stylish", "npm:4.1.0"],\
["@sidvind/better-ajv-errors", "virtual:2a2a921469e6f0bfdb6b2bd79f75a3395d47a481854507365048f3d989418f207cf814cb2ce1a012d2da774c1d130b4ca418582463ec08381da55e543b959c4c#npm:2.1.3"],\
["@sidvind/better-ajv-errors", "virtual:127f44ed2d4bdd83725c8820683da6ed922142e851761151ec4911dcca098a2d4d832b774a31e7abf98185d360ff83c999b319ea3314c86d38eca853072fb0d2#npm:2.1.3"],\
["@types/jest", null],\
["@types/jest-diff", null],\
["@types/jest-snapshot", null],\
@ -7153,6 +7207,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
["muggle-string", [\
["npm:0.4.1", {\
"packageLocation": "./.yarn/cache/muggle-string-npm-0.4.1-fe3c825cc2-85fe1766d1.zip/node_modules/muggle-string/",\
"packageDependencies": [\
["muggle-string", "npm:0.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["murmurhash-js", [\
["npm:1.0.0", {\
"packageLocation": "./.yarn/cache/murmurhash-js-npm-1.0.0-b1fa804bc0-083cea92a1.zip/node_modules/murmurhash-js/",\
@ -8269,13 +8332,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["@rollup/pluginutils", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.1.0"],\
["@twuni/emojify", "npm:1.0.2"],\
["@vitejs/plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.6.2"],\
["@vue/language-plugin-pug", "npm:2.0.7"],\
["bootstrap", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.3.3"],\
["bootstrap-icons", "npm:1.11.3"],\
["browser-fs-access", "npm:0.35.0"],\
["browserlist", "npm:1.0.1"],\
["c8", "npm:9.1.0"],\
["caniuse-lite", "npm:1.0.30001597"],\
["d3", "npm:7.8.5"],\
["caniuse-lite", "npm:1.0.30001599"],\
["d3", "npm:7.9.0"],\
["eslint", "npm:8.57.0"],\
["eslint-config-standard", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:17.1.0"],\
["eslint-plugin-cypress", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.15.1"],\
@ -8283,10 +8347,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["eslint-plugin-n", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:16.6.2"],\
["eslint-plugin-node", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:11.1.0"],\
["eslint-plugin-promise", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.1"],\
["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.22.0"],\
["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.23.0"],\
["file-saver", "npm:2.0.5"],\
["highcharts", "npm:11.4.0"],\
["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.15.0"],\
["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.16.0"],\
["ical.js", "npm:1.5.0"],\
["jquery", "npm:3.7.1"],\
["jquery-migrate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.1"],\
@ -8304,7 +8368,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.71.1"],\
["sass", "npm:1.72.0"],\
["seedrandom", "npm:3.0.5"],\
["select2", "npm:4.1.0-rc.0"],\
["select2-bootstrap-5-theme", "npm:1.3.0"],\
@ -8401,10 +8465,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "HARD"\
}],\
["npm:1.71.1", {\
"packageLocation": "./.yarn/cache/sass-npm-1.71.1-3aced13991-19c4939d30.zip/node_modules/sass/",\
["npm:1.72.0", {\
"packageLocation": "./.yarn/cache/sass-npm-1.72.0-fb38bb530c-f420079c7d.zip/node_modules/sass/",\
"packageDependencies": [\
["sass", "npm:1.71.1"],\
["sass", "npm:1.72.0"],\
["chokidar", "npm:3.5.3"],\
["immutable", "npm:4.0.0"],\
["source-map-js", "npm:1.0.2"]\
@ -9227,7 +9291,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
["lightningcss", null],\
["postcss", "npm:8.4.33"],\
["rollup", "npm:3.29.4"],\
["sass", "npm:1.71.1"],\
["sass", "npm:1.72.0"],\
["stylus", null],\
["sugarss", null],\
["terser", null]\
@ -9259,6 +9323,46 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
["volar-service-html", [\
["npm:0.0.34", {\
"packageLocation": "./.yarn/cache/volar-service-html-npm-0.0.34-32b6d24136-83b50cd805.zip/node_modules/volar-service-html/",\
"packageDependencies": [\
["volar-service-html", "npm:0.0.34"]\
],\
"linkType": "SOFT"\
}],\
["virtual:6f5429e17c4ecd390af605a4e97ecc7b34f2f1374a5e30c21f0a978cbdc904738a42d0d6f5d44d2e969250218b3c205853d6afefd88b87bcda877286d12bef83#npm:0.0.34", {\
"packageLocation": "./.yarn/__virtual__/volar-service-html-virtual-5a9107a24d/0/cache/volar-service-html-npm-0.0.34-32b6d24136-83b50cd805.zip/node_modules/volar-service-html/",\
"packageDependencies": [\
["volar-service-html", "virtual:6f5429e17c4ecd390af605a4e97ecc7b34f2f1374a5e30c21f0a978cbdc904738a42d0d6f5d44d2e969250218b3c205853d6afefd88b87bcda877286d12bef83#npm:0.0.34"],\
["@types/volar__language-service", null],\
["@volar/language-service", "npm:2.1.4"],\
["vscode-html-languageservice", "npm:5.1.2"],\
["vscode-languageserver-textdocument", "npm:1.0.11"],\
["vscode-uri", "npm:3.0.8"]\
],\
"packagePeers": [\
"@types/volar__language-service",\
"@volar/language-service"\
],\
"linkType": "HARD"\
}]\
]],\
["volar-service-pug", [\
["npm:0.0.34", {\
"packageLocation": "./.yarn/cache/volar-service-pug-npm-0.0.34-6f5429e17c-4691aa1c8e.zip/node_modules/volar-service-pug/",\
"packageDependencies": [\
["volar-service-pug", "npm:0.0.34"],\
["@volar/language-service", "npm:2.1.4"],\
["pug-lexer", "npm:5.0.1"],\
["pug-parser", "npm:6.0.0"],\
["volar-service-html", "virtual:6f5429e17c4ecd390af605a4e97ecc7b34f2f1374a5e30c21f0a978cbdc904738a42d0d6f5d44d2e969250218b3c205853d6afefd88b87bcda877286d12bef83#npm:0.0.34"],\
["vscode-html-languageservice", "npm:5.1.2"],\
["vscode-languageserver-textdocument", "npm:1.0.11"]\
],\
"linkType": "HARD"\
}]\
]],\
["vooks", [\
["npm:0.2.12", {\
"packageLocation": "./.yarn/cache/vooks-npm-0.2.12-0d1a2d856b-e6841ec5b6.zip/node_modules/vooks/",\
@ -9282,6 +9386,66 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
"linkType": "HARD"\
}]\
]],\
["vscode-html-languageservice", [\
["npm:5.1.2", {\
"packageLocation": "./.yarn/cache/vscode-html-languageservice-npm-5.1.2-2ea2618bdd-3a2a5ee5ad.zip/node_modules/vscode-html-languageservice/",\
"packageDependencies": [\
["vscode-html-languageservice", "npm:5.1.2"],\
["@vscode/l10n", "npm:0.0.18"],\
["vscode-languageserver-textdocument", "npm:1.0.11"],\
["vscode-languageserver-types", "npm:3.17.5"],\
["vscode-uri", "npm:3.0.8"]\
],\
"linkType": "HARD"\
}]\
]],\
["vscode-jsonrpc", [\
["npm:8.2.0", {\
"packageLocation": "./.yarn/cache/vscode-jsonrpc-npm-8.2.0-b7d2e5b553-f302a01e59.zip/node_modules/vscode-jsonrpc/",\
"packageDependencies": [\
["vscode-jsonrpc", "npm:8.2.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["vscode-languageserver-protocol", [\
["npm:3.17.5", {\
"packageLocation": "./.yarn/cache/vscode-languageserver-protocol-npm-3.17.5-2b07e16989-dfb42d276d.zip/node_modules/vscode-languageserver-protocol/",\
"packageDependencies": [\
["vscode-languageserver-protocol", "npm:3.17.5"],\
["vscode-jsonrpc", "npm:8.2.0"],\
["vscode-languageserver-types", "npm:3.17.5"]\
],\
"linkType": "HARD"\
}]\
]],\
["vscode-languageserver-textdocument", [\
["npm:1.0.11", {\
"packageLocation": "./.yarn/cache/vscode-languageserver-textdocument-npm-1.0.11-6fc94d2b7b-ea7cdc9d4f.zip/node_modules/vscode-languageserver-textdocument/",\
"packageDependencies": [\
["vscode-languageserver-textdocument", "npm:1.0.11"]\
],\
"linkType": "HARD"\
}]\
]],\
["vscode-languageserver-types", [\
["npm:3.17.5", {\
"packageLocation": "./.yarn/cache/vscode-languageserver-types-npm-3.17.5-aca3b71a5a-79b420e757.zip/node_modules/vscode-languageserver-types/",\
"packageDependencies": [\
["vscode-languageserver-types", "npm:3.17.5"]\
],\
"linkType": "HARD"\
}]\
]],\
["vscode-uri", [\
["npm:3.0.8", {\
"packageLocation": "./.yarn/cache/vscode-uri-npm-3.0.8-56f46b9d24-5142491268.zip/node_modules/vscode-uri/",\
"packageDependencies": [\
["vscode-uri", "npm:3.0.8"]\
],\
"linkType": "HARD"\
}]\
]],\
["vue", [\
["npm:3.4.21", {\
"packageLocation": "./.yarn/cache/vue-npm-3.4.21-02110aa6d9-3c477982a0.zip/node_modules/vue/",\
@ -9367,10 +9531,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) {
],\
"linkType": "SOFT"\
}],\
["virtual:2da7be1523058e3d6960a64ca4697b767ffeb995ed69da9eb74f6f820005e744683aa6cde73ca784c556dfc97ad5b1bc884e0f3f37187c87e646cf74b01fce0e#npm:9.4.2", {\
"packageLocation": "./.yarn/__virtual__/vue-eslint-parser-virtual-989a31128c/0/cache/vue-eslint-parser-npm-9.4.2-3e4e696025-67f14c8ea1.zip/node_modules/vue-eslint-parser/",\
["virtual:a9cf4d7c437b03eeeb49b01b4b3434ca26b6f59f98831796df73ffd0744dcbe1bfe9c60f8b7e2c803ab0f29084730afdc0220af1db5d8d3d00531e0df4f49dbe#npm:9.4.2", {\
"packageLocation": "./.yarn/__virtual__/vue-eslint-parser-virtual-6710b98248/0/cache/vue-eslint-parser-npm-9.4.2-3e4e696025-67f14c8ea1.zip/node_modules/vue-eslint-parser/",\
"packageDependencies": [\
["vue-eslint-parser", "virtual:2da7be1523058e3d6960a64ca4697b767ffeb995ed69da9eb74f6f820005e744683aa6cde73ca784c556dfc97ad5b1bc884e0f3f37187c87e646cf74b01fce0e#npm:9.4.2"],\
["vue-eslint-parser", "virtual:a9cf4d7c437b03eeeb49b01b4b3434ca26b6f59f98831796df73ffd0744dcbe1bfe9c60f8b7e2c803ab0f29084730afdc0220af1db5d8d3d00531e0df4f49dbe#npm:9.4.2"],\
["@types/eslint", null],\
["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\
["eslint", "npm:8.57.0"],\

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

@ -47,8 +47,5 @@ $DTDIR/ietf/manage.py run_yang_model_checks -v0
# Enable when removed from /a/www/ietf-datatracker/scripts/Cron-runner:
$DTDIR/ietf/bin/expire-last-calls
# Send reminders originating from the review app
$DTDIR/ietf/bin/send-review-reminders
# Purge older PersonApiKeyEvents
$DTDIR/ietf/manage.py purge_old_personal_api_key_events 14

View file

@ -58,14 +58,17 @@
n-button(
:type='agendaStore.isTimezoneMeeting ? `primary` : `default`'
@click='setTimezone(`meeting`)'
:text-color='agendaStore.isTimezoneMeeting ? `#FFF` : null'
) Meeting
n-button(
:type='agendaStore.isTimezoneLocal ? `primary` : `default`'
@click='setTimezone(`local`)'
:text-color='agendaStore.isTimezoneLocal ? `#FFF` : null'
) Local
n-button(
:type='agendaStore.timezone === `UTC` ? `primary` : `default`'
@click='setTimezone(`UTC`)'
:text-color='agendaStore.timezone === `UTC` ? `#FFF` : null'
) UTC
n-select.agenda-timezone-ddn(
v-if='siteStore.viewport > 1250'

View file

@ -62,7 +62,8 @@
n-button.mt-2(
id='agenda-quickaccess-calview-btn'
block
color='#6c757d'
color='#6f42c1'
text-color='#FFF'
size='large'
strong
@click='agendaStore.$patch({ calendarShown: true })'
@ -78,8 +79,8 @@
n-button.mt-2(
id='agenda-quickaccess-addtocal-btn'
block
secondary
color='#6c757d'
:color='siteStore.theme === `dark` ? `rgba(111, 66, 193, .3)` : `#e2d9f3`'
:text-color='siteStore.theme === `dark` ? `#e2d9f3` : `#59359a`'
size='large'
strong
)

View file

@ -11,14 +11,17 @@ n-drawer(v-model:show='isShown', placement='bottom', :height='state.drawerHeight
n-button(
:type='agendaStore.isTimezoneMeeting ? `primary` : `default`'
@click='setTimezone(`meeting`)'
:text-color='agendaStore.isTimezoneMeeting ? `#FFF` : null'
) Meeting
n-button(
:type='agendaStore.isTimezoneLocal ? `primary` : `default`'
@click='setTimezone(`local`)'
:text-color='agendaStore.isTimezoneLocal ? `#FFF` : null'
) Local
n-button(
:type='agendaStore.timezone === `UTC` ? `primary` : `default`'
@click='setTimezone(`UTC`)'
:text-color='agendaStore.timezone === `UTC` ? `#FFF` : null'
) UTC
n-divider(vertical)
n-button.me-2(
@ -32,7 +35,7 @@ n-drawer(v-model:show='isShown', placement='bottom', :height='state.drawerHeight
n-badge.ms-2(:value='agendaStore.selectedCatSubs.length', processing)
n-button(
ghost
color='gray'
:color='siteStore.theme === `dark` ? `#e35d6a` : `gray`'
strong
@click='close'
)

View file

@ -3,9 +3,11 @@
n-data-table(
v-if='state.items.length > 0'
:data='state.items'
:columns='columns'
:columns='state.columns'
striped
)
span.text-danger(v-else-if='state.errMessage')
em {{ state.errMessage }}
span.text-body-secondary(v-else)
em No polls available.
</template>
@ -13,9 +15,8 @@
<script setup>
import { onMounted, reactive } from 'vue'
import { DateTime } from 'luxon'
import {
NDataTable
} from 'naive-ui'
import { cloneDeep, startCase } from 'lodash-es'
import { NDataTable } from 'naive-ui'
// PROPS
@ -29,13 +30,15 @@ const props = defineProps({
// STATE
const state = reactive({
items: []
items: [],
colums: [],
errMessage: null
})
const columns = [
const defaultColumns = [
{
title: 'Question',
key: 'question'
key: 'text'
},
{
title: 'Start Time',
@ -44,14 +47,6 @@ const columns = [
{
title: 'End Time',
key: 'end_time'
},
{
title: 'Raise Hand',
key: 'raise_hand'
},
{
title: 'Do Not Raise Hand',
key: 'do_not_raise_hand'
}
]
@ -59,20 +54,38 @@ const columns = [
onMounted(() => {
// Get polls from embedded json tag
const polls = JSON.parse(document.getElementById(`${props.componentId}-data`).textContent || '[]')
if (polls.length > 0) {
let idx = 1
for (const poll of polls) {
state.items.push({
id: `poll-${idx}`,
question: poll.text,
start_time: DateTime.fromISO(poll.start_time).toFormat('dd LLLL yyyy \'at\' HH:mm:ss a ZZZZ'),
end_time: DateTime.fromISO(poll.end_time).toFormat('dd LLLL yyyy \'at\' HH:mm:ss a ZZZZ'),
raise_hand: poll.raise_hand,
do_not_raise_hand: poll.do_not_raise_hand
})
idx++
try {
const polls = JSON.parse(document.getElementById(`${props.componentId}-data`).textContent || '[]')
if (polls.length > 0) {
// Populate columns
state.columns = cloneDeep(defaultColumns)
for (const col in polls[0]) {
if (!['text', 'start_time', 'end_time'].includes(col)) {
state.columns.push({
title: startCase(col),
key: col,
minWidth: 100,
titleAlign: 'center',
align: 'center'
})
}
}
// Populate rows
let idx = 1
for (const poll of polls) {
state.items.push({
...poll,
id: `poll-${idx}`,
start_time: DateTime.fromISO(poll.start_time).toFormat('dd LLLL yyyy \'at\' HH:mm:ss a ZZZZ'),
end_time: DateTime.fromISO(poll.end_time).toFormat('dd LLLL yyyy \'at\' HH:mm:ss a ZZZZ')
})
idx++
}
}
} catch (err) {
console.warn(err)
state.errMessage = 'Failed to load poll results.'
}
})
</script>

View file

@ -55,7 +55,7 @@ def trace(fn): # renamed from 'report' by henrik 16 Jun 2011
if len(s) > n+3:
s = s[:n]+"..."
return s
def wrap(fn, *params,**kwargs):
def wrap(*params,**kwargs):
call = wrap.callcount = wrap.callcount + 1
indent = ' ' * _report_indent[0]
@ -81,8 +81,8 @@ def trace(fn): # renamed from 'report' by henrik 16 Jun 2011
return ret
wrap.callcount = 0
if debug:
from decorator import decorator
return decorator(wrap, fn)
from functools import update_wrapper
return update_wrapper(wrap, fn)
else:
return fn
@ -119,7 +119,7 @@ def clock(s):
def time(fn):
"""Decorator to print timing information about a function call.
"""
def wrap(fn, *params,**kwargs):
def wrap(*params,**kwargs):
indent = ' ' * _report_indent[0]
fc = "%s.%s()" % (fn.__module__, fn.__name__,)
@ -132,8 +132,8 @@ def time(fn):
return ret
wrap.callcount = 0
if debug:
from decorator import decorator
return decorator(wrap, fn)
from functools import update_wrapper
return update_wrapper(wrap, fn)
else:
return fn
@ -190,7 +190,7 @@ def type(name):
value = eval(name, frame.f_globals, frame.f_locals)
indent = ' ' * (_report_indent[0])
sys.stderr.write("%s%s: %s\n" % (indent, name, value))
def say(s):
if debug:
indent = ' ' * (_report_indent[0])
@ -205,11 +205,11 @@ def profile(fn):
prof.dump_stats(datafn)
return retval
if debug:
from decorator import decorator
return decorator(wrapper, fn)
from functools import update_wrapper
return update_wrapper(wrapper, fn)
else:
return fn
def traceback(levels=None):
if debug:
indent = ' ' * (_report_indent[0])

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.15"
"npm-check-updates": "16.14.17"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -4079,9 +4079,9 @@
}
},
"node_modules/npm-check-updates": {
"version": "16.14.15",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz",
"integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==",
"version": "16.14.17",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.17.tgz",
"integrity": "sha512-ElnDdXKe60f8S6RhzFeaGuH2TFJmt2cU2HjLdowldabdm27nWFCxV2ebeP3xGbQkzp2+RPDQNdW9HqU1lcY8ag==",
"dev": true,
"dependencies": {
"chalk": "^5.3.0",
@ -9249,9 +9249,9 @@
}
},
"npm-check-updates": {
"version": "16.14.15",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz",
"integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==",
"version": "16.14.17",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.17.tgz",
"integrity": "sha512-ElnDdXKe60f8S6RhzFeaGuH2TFJmt2cU2HjLdowldabdm27nWFCxV2ebeP3xGbQkzp2+RPDQNdW9HqU1lcY8ag==",
"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.15"
"npm-check-updates": "16.14.17"
}
}

View file

@ -42,14 +42,14 @@ def check_group_email_aliases_exists(app_configs, **kwargs):
if not ok:
errors.append(checks.Error(
"Found no aliases in the group email aliases file\n'%s'."%settings.GROUP_ALIASES_PATH,
hint="Please run the generate_group_aliases management command to generate them.",
hint="These should be created by the infrastructure using ietf/bin/aliases-from-json.py.",
obj=None,
id="datatracker.E0002",
))
except IOError as e:
errors.append(checks.Error(
"Could not read group email aliases:\n %s" % e,
hint="Please run the generate_group_aliases management command to generate them.",
hint="These should be created by the infrastructure using ietf/bin/aliases-from-json.py.",
obj=None,
id="datatracker.E0003",
))
@ -69,14 +69,14 @@ def check_doc_email_aliases_exists(app_configs, **kwargs):
if not ok:
errors.append(checks.Error(
"Found no aliases in the document email aliases file\n'%s'."%settings.DRAFT_VIRTUAL_PATH,
hint="Please run the generate_draft_aliases management command to generate them.",
hint="These should be created by the infrastructure using ietf/bin/aliases-from-json.py.",
obj=None,
id="datatracker.E0004",
))
except IOError as e:
errors.append(checks.Error(
"Could not read document email aliases:\n %s" % e,
hint="Please run the generate_draft_aliases management command to generate them.",
hint="These should be created by the infrastructure using ietf/bin/aliases-from-json.py.",
obj=None,
id="datatracker.E0005",
))

View file

@ -1,183 +0,0 @@
# Copyright The IETF Trust 2012-2021, All Rights Reserved
# -*- coding: utf-8 -*-
# This was written as a script by Markus Stenberg <markus.stenberg@iki.fi>.
# It was turned into a management command by Russ Housley <housley@vigisec.com>.
import datetime
import io
import os
import re
import shutil
import stat
import time
from tempfile import mkstemp
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils import timezone
import debug # pyflakes:ignore
from ietf.doc.models import Document
from ietf.group.utils import get_group_role_emails, get_group_ad_emails
from ietf.utils.aliases import dump_sublist
from utils.mail import parseaddr
from ietf.utils import log
DEFAULT_YEARS = 2
def get_draft_ad_emails(doc):
"""Get AD email addresses for the given draft, if any."""
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(doc):
"""Get chair email addresses for the given draft, if any."""
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(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(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(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(get_draft_ad_emails(doc))
elif re.search(author_email_alias_regex, e):
notify_emails.update(get_draft_authors_emails(doc))
elif re.search(shepherd_email_alias_regex, e):
notify_emails.update(get_draft_shepherd_email(doc))
elif re.search(all_email_alias_regex, e):
notify_emails.update(get_draft_ad_emails(doc))
notify_emails.update(get_draft_authors_emails(doc))
notify_emails.update(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
class Command(BaseCommand):
help = ('Generate the draft-aliases and draft-virtual files for Internet-Draft '
'mail aliases, placing them in the files configured in '
'settings.DRAFT_ALIASES_PATH and settings.DRAFT_VIRTUAL_PATH, '
'respectively. The generation includes aliases for Internet-Drafts '
'that have seen activity in the last %s years.' % (DEFAULT_YEARS))
def handle(self, *args, **options):
show_since = timezone.now() - datetime.timedelta(DEFAULT_YEARS*365)
date = time.strftime("%Y-%m-%d_%H:%M:%S")
signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date)
ahandle, aname = mkstemp()
os.close(ahandle)
afile = io.open(aname,"w")
vhandle, vname = mkstemp()
os.close(vhandle)
vfile = io.open(vname,"w")
afile.write(signature)
vfile.write(signature)
vfile.write("%s anything\n" % settings.DRAFT_VIRTUAL_DOMAIN)
# Internet-Drafts with active status or expired within DEFAULT_YEARS
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
alias_domains = ['ietf.org', ]
for draft in interesting_drafts.distinct().iterator():
# Omit drafts that became RFCs, unless they were published in the last DEFAULT_YEARS
if draft.get_state_slug()=="rfc":
rfc = draft.became_rfc()
log.assertion("rfc is not None")
if rfc.latest_event(type='published_rfc').time < show_since:
continue
alias = draft.name
all = set()
# no suffix and .authors are the same list
emails = get_draft_authors_emails(draft)
all.update(emails)
dump_sublist(afile, vfile, alias, alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails)
dump_sublist(afile, vfile, alias+'.authors', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails)
# .chairs = group chairs
emails = get_draft_chair_emails(draft)
if emails:
all.update(emails)
dump_sublist(afile, vfile, alias+'.chairs', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails)
# .ad = sponsoring AD / WG AD (WG document)
emails = get_draft_ad_emails(draft)
if emails:
all.update(emails)
dump_sublist(afile, vfile, alias+'.ad', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails)
# .notify = notify email list from the Document
emails = get_draft_notify_emails(draft)
if emails:
all.update(emails)
dump_sublist(afile, vfile, alias+'.notify', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails)
# .shepherd = shepherd email from the Document
emails = get_draft_shepherd_email(draft)
if emails:
all.update(emails)
dump_sublist(afile, vfile, alias+'.shepherd', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails)
# .all = everything from above
dump_sublist(afile, vfile, alias+'.all', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, all)
afile.close()
vfile.close()
os.chmod(aname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
os.chmod(vname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
shutil.move(aname, settings.DRAFT_ALIASES_PATH)
shutil.move(vname, settings.DRAFT_VIRTUAL_PATH)

View file

@ -881,3 +881,21 @@ def badgeify(blob):
)
return text
@register.filter
def simple_history_delta_changes(history):
"""Returns diff between given history and previous entry."""
prev = history.prev_record
if prev:
delta = history.diff_against(prev)
return delta.changes
return []
@register.filter
def simple_history_delta_change_cnt(history):
"""Returns number of changes between given history and previous entry."""
prev = history.prev_record
if prev:
delta = history.diff_against(prev)
return len(delta.changes)
return 0

View file

@ -37,7 +37,7 @@ import debug # pyflakes:ignore
from ietf.doc.models import ( Document, DocRelationshipName, RelatedDocument, State,
DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, NewRevisionDocEvent, BallotType,
EditedAuthorsDocEvent )
EditedAuthorsDocEvent, StateType)
from ietf.doc.factories import ( DocumentFactory, DocEventFactory, CharterFactory,
ConflictReviewFactory, WgDraftFactory, IndividualDraftFactory, WgRfcFactory,
IndividualRfcFactory, StateDocEventFactory, BallotPositionDocEventFactory,
@ -2181,152 +2181,6 @@ class GenerateDraftAliasesTests(TestCase):
os.unlink(self.doc_virtual_file.name)
super().tearDown()
def testManagementCommand(self):
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
)
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])
args = []
kwargs = {}
out = io.StringIO()
call_command("generate_draft_aliases", *args, **kwargs, stdout=out, stderr=out)
self.assertFalse(out.getvalue())
with open(settings.DRAFT_ALIASES_PATH) as afile:
acontent = afile.read()
for x in [
"xfilter-" + doc1.name,
"xfilter-" + doc1.name + ".ad",
"xfilter-" + doc1.name + ".authors",
"xfilter-" + doc1.name + ".shepherd",
"xfilter-" + doc1.name + ".all",
"xfilter-" + doc2.name,
"xfilter-" + doc2.name + ".ad",
"xfilter-" + doc2.name + ".authors",
"xfilter-" + doc2.name + ".chairs",
"xfilter-" + doc2.name + ".all",
"xfilter-" + doc3.name,
"xfilter-" + doc3.name + ".ad",
"xfilter-" + doc3.name + ".authors",
"xfilter-" + doc3.name + ".chairs",
"xfilter-" + doc5.name,
"xfilter-" + doc5.name + ".authors",
"xfilter-" + doc5.name + ".all",
]:
self.assertIn(x, acontent)
for x in [
"xfilter-" + doc1.name + ".chairs",
"xfilter-" + doc2.name + ".shepherd",
"xfilter-" + doc3.name + ".shepherd",
"xfilter-" + doc4.name,
"xfilter-" + doc5.name + ".shepherd",
"xfilter-" + doc5.name + ".ad",
]:
self.assertNotIn(x, acontent)
with open(settings.DRAFT_VIRTUAL_PATH) as vfile:
vcontent = vfile.read()
for x in [
ad.email_address(),
shepherd.email_address(),
marschairman.email_address(),
author1.email_address(),
author2.email_address(),
author3.email_address(),
author6.email_address(),
]:
self.assertIn(x, vcontent)
for x in [
author4.email_address(),
author5.email_address(),
]:
self.assertNotIn(x, vcontent)
for x in [
"xfilter-" + doc1.name,
"xfilter-" + doc1.name + ".ad",
"xfilter-" + doc1.name + ".authors",
"xfilter-" + doc1.name + ".shepherd",
"xfilter-" + doc1.name + ".all",
"xfilter-" + doc2.name,
"xfilter-" + doc2.name + ".ad",
"xfilter-" + doc2.name + ".authors",
"xfilter-" + doc2.name + ".chairs",
"xfilter-" + doc2.name + ".all",
"xfilter-" + doc3.name,
"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",
]:
self.assertIn(x, vcontent)
for x in [
"xfilter-" + doc1.name + ".chairs",
"xfilter-" + doc2.name + ".shepherd",
"xfilter-" + doc3.name + ".shepherd",
"xfilter-" + doc4.name,
"xfilter-" + doc5.name + ".shepherd",
"xfilter-" + doc5.name + ".ad",
]:
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"""
@ -3273,3 +3127,17 @@ class DocInfoMethodsTests(TestCase):
rfc.referenced_by_rfcs_as_rfc_or_draft(),
draft.targets_related.filter(source__type="rfc") | rfc.targets_related.filter(source__type="rfc"),
)
class StateIndexTests(TestCase):
def test_state_index(self):
url = urlreverse('ietf.doc.views_help.state_index')
r = self.client.get(url)
q = PyQuery(r.content)
content = [ e.text for e in q('#content table td a ') ]
names = StateType.objects.values_list('slug', flat=True)
# The following doesn't cover all doc types, only a selection
for name in names:
if not '-' in name:
self.assertIn(name, content)

View file

@ -20,7 +20,7 @@ from django.utils.html import escape
import debug # pyflakes:ignore
from ietf.doc.expire import get_expired_drafts, send_expire_notice_for_draft, expire_draft
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, RgDraftFactory, DocEventFactory
from ietf.doc.factories import EditorialDraftFactory, IndividualDraftFactory, WgDraftFactory, RgDraftFactory, DocEventFactory
from ietf.doc.models import ( Document, DocReminder, DocEvent,
ConsensusDocEvent, LastCallDocEvent, RelatedDocument, State, TelechatDocEvent,
WriteupDocEvent, DocRelationshipName, IanaExpertDocEvent )
@ -2161,3 +2161,13 @@ class ShepherdWriteupTests(TestCase):
self.assertContains(r, "for Group Documents", status_code=200)
r = self.client.post(url,dict(reset_text=''))
self.assertContains(r, "for Group Documents", status_code=200)
class EditorialDraftMetadataTests(TestCase):
def test_editorial_metadata(self):
draft = EditorialDraftFactory()
url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))
r = self.client.get(url)
q = PyQuery(r.content)
top_level_metadata_headings = q("tbody>tr>th:first-child").text()
self.assertNotIn("IESG", top_level_metadata_headings)
self.assertNotIn("IANA", top_level_metadata_headings)

View file

@ -163,6 +163,7 @@ urlpatterns = [
url(r'^%(name)s/edit/issueballot/rsab/$' % settings.URL_REGEXPS, views_ballot.issue_rsab_ballot),
url(r'^%(name)s/edit/closeballot/rsab/$' % settings.URL_REGEXPS, views_ballot.close_rsab_ballot),
url(r'^help/state/?$', views_help.state_index),
url(r'^help/state/(?P<type>[\w-]+)/$', views_help.state_help),
url(r'^help/relationships/$', views_help.relationship_help),
url(r'^help/relationships/(?P<subset>\w+)/$', views_help.relationship_help),

View file

@ -1062,6 +1062,8 @@ def build_file_urls(doc: Union[Document, DocHistory]):
base = settings.IETF_ID_ARCHIVE_URL
file_urls = []
for t in found_types:
if t == "ps": # Postscript might have been submitted but should not be displayed in the list of URLs
continue
label = "plain text" if t == "txt" else t
file_urls.append((label, base + doc.name + "-" + doc.rev + "." + t))

View file

@ -9,6 +9,18 @@ from ietf.doc.models import State, StateType, IESG_SUBSTATE_TAGS
from ietf.name.models import DocRelationshipName, DocTagName
from ietf.doc.utils import get_tags_for_stream_id
def state_index(request):
types = StateType.objects.all()
names = [ type.slug for type in types ]
for type in types:
if "-" in type.slug and type.slug.split('-',1)[0] in names:
type.stategroups = None
else:
groups = StateType.objects.filter(slug__startswith=type.slug)
type.stategroups = [ g.slug[len(type.slug)+1:] for g in groups if not g == type ] or ""
return render(request, 'doc/state_index.html', {"types": types})
def state_help(request, type=None):
slug, title = {
"draft-iesg": ("draft-iesg", "IESG States for Internet-Drafts"),
@ -26,7 +38,30 @@ def state_help(request, type=None):
"status-change": ("statchg", "RFC Status Change States"),
"bofreq": ("bofreq", "BOF Request States"),
"procmaterials": ("procmaterials", "Proceedings Materials States"),
"statement": {"statement", "Statement States"}
"statement": ("statement", "Statement States"),
"slides": ("slides", "Slides States"),
"minutes": ("minutes", "Minutes States"),
"liai-att": ("liai-att", "Liaison Attachment States"),
"recording": ("recording", "Recording States"),
"bluesheets": ("bluesheets", "Bluesheets States"),
"reuse_policy": ("reuse_policy", "Reuse Policy States"),
"review": ("review", "Review States"),
"liaison": ("liaison", "Liaison States"),
"shepwrit": ("shepwrit", "Shapherd Writeup States"),
"bofreq": ("bofreq", "BOF Request States"),
"procmaterials": ("procmaterials", "Proceedings Materials States"),
"chatlog": ("chatlog", "Chat Log States"),
"polls": ("polls", "Polls States"),
"statement": ("statement", "Statement States"),
"rfc": ("rfc", "RFC States"),
"bcp": ("bcp", "BCP States"),
"std": ("std", "STD States"),
"fyi": ("fyi", "FYI States"),
"narrativeminutes": ("narrativeminutes", "Narrative Minutes States"),
"draft": ("draft", "Draft States"),
"statchg": ("statchg", "Status Change States"),
"agenda": ("agenda", "Agenda States"),
"conflrev": ("conflrev", "Conflict Review States")
}.get(type, (None, None))
state_type = get_object_or_404(StateType, slug=slug)

View file

@ -1053,7 +1053,11 @@ def edit_deadline(request, name, request_id):
if form.is_valid():
if form.cleaned_data['deadline'] != old_deadline:
form.save()
subject = "Deadline changed: {} {} review of {}-{}".format(review_req.team.acronym.capitalize(),review_req.type.name.lower(), review_req.doc.name, review_req.requested_rev)
subject = f"Deadline changed: {review_req.team.acronym.capitalize()} {review_req.type.name.lower()} review of {review_req.doc.name}"
if review_req.requested_rev:
subject += f"-{review_req.requested_rev}"
descr = "Deadine changed from {} to {}".format(old_deadline, review_req.deadline)
update_change_reason(review_req, descr)
msg = render_to_string("review/deadline_changed.txt", {
"review_req": review_req,
"old_deadline": old_deadline,

View file

@ -1,105 +0,0 @@
# Copyright The IETF Trust 2012-2021, All Rights Reserved
# -*- coding: utf-8 -*-
# This was written as a script by Markus Stenberg <markus.stenberg@iki.fi>.
# It was turned into a management command by Russ Housley <housley@vigisec.com>.
import datetime
import io
import os
import shutil
import stat
import time
from tempfile import mkstemp
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils import timezone
import debug # pyflakes:ignore
from ietf.group.models import Group
from ietf.group.utils import get_group_ad_emails, get_group_role_emails, get_child_group_role_emails
from ietf.name.models import GroupTypeName
from ietf.utils.aliases import dump_sublist
DEFAULT_YEARS = 5
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']
IETF_DOMAIN=['ietf.org', ]
IRTF_DOMAIN=['irtf.org', ]
IAB_DOMAIN=['iab.org', ]
class Command(BaseCommand):
help = ('Generate the group-aliases and group-virtual files for Internet-Draft '
'mail aliases, placing them in the file configured in '
'settings.GROUP_ALIASES_PATH and settings.GROUP_VIRTUAL_PATH, '
'respectively. The generation includes aliases for groups that '
'have seen activity in the last %s years.' % (DEFAULT_YEARS))
def handle(self, *args, **options):
show_since = timezone.now() - datetime.timedelta(DEFAULT_YEARS*365)
date = time.strftime("%Y-%m-%d_%H:%M:%S")
signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date)
ahandle, aname = mkstemp()
os.close(ahandle)
afile = io.open(aname,"w")
vhandle, vname = mkstemp()
os.close(vhandle)
vfile = io.open(vname,"w")
afile.write(signature)
vfile.write(signature)
vfile.write("%s anything\n" % settings.GROUP_VIRTUAL_DOMAIN)
# Loop through each group type and build -ads and -chairs entries
for g in GROUP_TYPES:
domains = []
domains += IETF_DOMAIN
if g in ('rg', 'rag'):
domains += IRTF_DOMAIN
if g == 'program':
domains += IAB_DOMAIN
entries = Group.objects.filter(type=g).all()
active_entries = entries.filter(state__in=ACTIVE_STATES)
inactive_recent_entries = entries.exclude(state__in=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 NO_AD_GROUP_TYPES:
dump_sublist(afile, vfile, name+'-ads', domains, settings.GROUP_VIRTUAL_DOMAIN, get_group_ad_emails(e))
# All group types have -chairs lists
dump_sublist(afile, vfile, name+'-chairs', domains, settings.GROUP_VIRTUAL_DOMAIN, get_group_role_emails(e, ['chair', 'secr']))
# 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=ACTIVE_STATES)
for area in active_areas:
name = area.acronym
area_ad_emails = get_group_role_emails(area, ['pre-ad', 'ad', 'chair'])
dump_sublist(afile, vfile, name+'-ads', IETF_DOMAIN, settings.GROUP_VIRTUAL_DOMAIN, area_ad_emails)
dump_sublist(afile, vfile, name+'-chairs', IETF_DOMAIN, settings.GROUP_VIRTUAL_DOMAIN, (get_child_group_role_emails(area, ['chair', 'secr']) | area_ad_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:
dump_sublist(afile, vfile, group.acronym+'-chairs', IETF_DOMAIN, settings.GROUP_VIRTUAL_DOMAIN, get_group_role_emails(group, ['chair', 'delegate']))
afile.close()
vfile.close()
os.chmod(aname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
os.chmod(vname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
shutil.move(aname, settings.GROUP_ALIASES_PATH)
shutil.move(vname, settings.GROUP_VIRTUAL_PATH)

View file

@ -1,14 +1,12 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import io
import os
import datetime
import json
from tempfile import NamedTemporaryFile
from django.core.management import call_command
from django.conf import settings
from django.urls import reverse as urlreverse
from django.db.models import Q
@ -146,108 +144,6 @@ class GenerateGroupAliasesTests(TestCase):
os.unlink(self.doc_virtual_file.name)
super().tearDown()
def testManagementCommand(self):
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())
role2 = RoleFactory(name_id='ad', group__type_id='area', group__acronym='done', group__state_id='conclude')
done = role2.group
done_ad = role2.person
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())
individual = PersonFactory()
args = [ ]
kwargs = { }
out = io.StringIO()
call_command("generate_group_aliases", *args, **kwargs, stdout=out, stderr=out)
self.assertFalse(out.getvalue())
with open(settings.GROUP_ALIASES_PATH) as afile:
acontent = afile.read()
self.assertTrue('xfilter-' + area.acronym + '-ads' in acontent)
self.assertTrue('xfilter-' + area.acronym + '-chairs' in acontent)
self.assertTrue('xfilter-' + mars.acronym + '-ads' in acontent)
self.assertTrue('xfilter-' + mars.acronym + '-chairs' in acontent)
self.assertTrue('xfilter-' + ames.acronym + '-ads' in acontent)
self.assertTrue('xfilter-' + ames.acronym + '-chairs' in acontent)
self.assertTrue(all([x in acontent for x in [
'xfilter-' + area.acronym + '-ads',
'xfilter-' + area.acronym + '-chairs',
'xfilter-' + mars.acronym + '-ads',
'xfilter-' + mars.acronym + '-chairs',
'xfilter-' + ames.acronym + '-ads',
'xfilter-' + ames.acronym + '-chairs',
'xfilter-' + recent.acronym + '-ads',
'xfilter-' + recent.acronym + '-chairs',
]]))
self.assertFalse(all([x in acontent for x in [
'xfilter-' + done.acronym + '-ads',
'xfilter-' + done.acronym + '-chairs',
'xfilter-' + wayold.acronym + '-ads',
'xfilter-' + wayold.acronym + '-chairs',
]]))
with open(settings.GROUP_VIRTUAL_PATH) as vfile:
vcontent = vfile.read()
self.assertTrue(all([x in vcontent for x in [
ad.email_address(),
marschair.email_address(),
marssecr.email_address(),
ameschair.email_address(),
recentchair.email_address(),
testrgchair.email_address(),
testragchair.email_address(),
]]))
self.assertFalse(any([x in vcontent for x in [
done_ad.email_address(),
wayoldchair.email_address(),
individual.email_address(),
]]))
self.assertTrue(all([x in vcontent for x in [
'xfilter-' + area.acronym + '-ads',
'xfilter-' + area.acronym + '-chairs',
'xfilter-' + mars.acronym + '-ads',
'xfilter-' + mars.acronym + '-chairs',
'xfilter-' + ames.acronym + '-ads',
'xfilter-' + ames.acronym + '-chairs',
'xfilter-' + recent.acronym + '-ads',
'xfilter-' + recent.acronym + '-chairs',
'xfilter-' + testrg.acronym + '-chairs',
'xfilter-' + testrag.acronym + '-chairs',
testrg.acronym + '-chairs@ietf.org',
testrg.acronym + '-chairs@irtf.org',
testrag.acronym + '-chairs@ietf.org',
testrag.acronym + '-chairs@irtf.org',
]]))
self.assertFalse(all([x in vcontent for x in [
'xfilter-' + done.acronym + '-ads',
'xfilter-' + done.acronym + '-chairs',
'xfilter-' + wayold.acronym + '-ads',
'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

View file

@ -1,21 +0,0 @@
from pyquery import PyQuery
from django.urls import reverse
import debug # pyflakes:ignore
from ietf.utils.test_utils import TestCase
from ietf.doc.models import StateType
class HelpPageTests(TestCase):
def test_state_index(self):
url = reverse('ietf.help.views.state_index')
r = self.client.get(url)
q = PyQuery(r.content)
content = [ e.text for e in q('#content table td a ') ]
names = StateType.objects.values_list('slug', flat=True)
# The following doesn't cover all doc types, only a selection
for name in names:
if not '-' in name:
self.assertIn(name, content)

View file

@ -2,10 +2,10 @@
from ietf.help import views
from ietf.utils.urls import url
from django.views.generic import RedirectView
urlpatterns = [
url(r'^state/(?P<doc>[-\w]+)/(?P<type>[-\w]+)/?$', views.state),
url(r'^state/(?P<doc>[-\w]+)/?$', views.state),
url(r'^state/?$', views.state_index),
url(r'^state/?$', RedirectView.as_view(url='/doc/help/state/', permanent=True)),
]

View file

@ -1,23 +1,11 @@
# Copyright The IETF Trust 2007, All Rights Reserved
from django.shortcuts import get_object_or_404, render
import debug # pyflakes:ignore
from ietf.doc.models import State, StateType
from ietf.name.models import StreamName
from django.shortcuts import redirect
def state_index(request):
types = StateType.objects.all()
names = [ type.slug for type in types ]
for type in types:
if "-" in type.slug and type.slug.split('-',1)[0] in names:
type.stategroups = None
else:
groups = StateType.objects.filter(slug__startswith=type.slug)
type.stategroups = [ g.slug[len(type.slug)+1:] for g in groups if not g == type ] or ""
return render(request, 'help/state_index.html', {"types": types})
# This is just a redirect to the new URL under /doc; can probably go away eventually.
def state(request, doc, type=None):
if type:
@ -25,6 +13,5 @@ def state(request, doc, type=None):
if type in streams:
type = "stream-%s" % type
slug = "%s-%s" % (doc,type) if type else doc
statetype = get_object_or_404(StateType, slug=slug)
states = State.objects.filter(used=True, type=statetype).order_by('order')
return render(request, 'help/states.html', {"doc": doc, "type": statetype, "states":states} )
return redirect('/doc/help/state/%s' % slug, permanent = True)

View file

@ -149,9 +149,5 @@ def gather_relevant_expansions(**kwargs):
return sorted(rule_list)
def get_base_submission_message_address():
return Recipient.objects.get(slug="submission_manualpost_handling").gather()[0]
def get_base_ipr_request_address():
return Recipient.objects.get(slug="ipr_requests").gather()[0]

View file

@ -8598,7 +8598,7 @@ class ProceedingsTests(BaseMeetingTestCase):
self.assertEqual(r.status_code, 200)
self.assertContains(r, '3 attendees')
for person in persons:
self.assertContains(r, person.name)
self.assertContains(r, person.plain_name())
# Test for the "I was there" button.
def _test_button(person, expected):
@ -8618,14 +8618,14 @@ class ProceedingsTests(BaseMeetingTestCase):
# attempt to POST anyway is ignored
r = self.client.post(attendance_url)
self.assertEqual(r.status_code, 200)
self.assertNotContains(r, persons[3].name)
self.assertNotContains(r, persons[3].plain_name())
self.assertEqual(session.attended_set.count(), 3)
# button is shown, and POST is accepted
meeting.importantdate_set.update(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
_test_button(persons[3], True)
r = self.client.post(attendance_url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, persons[3].name)
self.assertContains(r, persons[3].plain_name())
self.assertEqual(session.attended_set.count(), 4)
# When the meeting is finalized, a bluesheet file is generated,
@ -8639,7 +8639,7 @@ class ProceedingsTests(BaseMeetingTestCase):
text = doc.text()
self.assertIn('4 attendees', text)
for person in persons:
self.assertIn(person.name, text)
self.assertIn(person.plain_name(), text)
r = self.client.get(session_url)
self.assertContains(r, doc.get_href())
self.assertNotContains(r, attendance_url)

View file

@ -2604,6 +2604,19 @@
"model": "doc.state",
"pk": 179
},
{
"fields": {
"desc": "The editorial stream processing of this document is complete and it has been sent to the RFC Editor for publication. The document may be in the RFC Editor's queue, or it may have been published as an RFC; this state doesn't distinguish between different states occurring after the document has left the RSAB.",
"name": "Sent to the RFC Editor",
"next_states": [],
"order": 10,
"slug": "rfc-edit",
"type": "draft-stream-editorial",
"used": true
},
"model": "doc.state",
"pk": 180
},
{
"fields": {
"label": "State"
@ -2660,13 +2673,6 @@
"model": "doc.statetype",
"pk": "draft"
},
{
"fields": {
"label": "IANA state"
},
"model": "doc.statetype",
"pk": "draft-iana"
},
{
"fields": {
"label": "IANA Action state"
@ -11286,8 +11292,8 @@
},
{
"fields": {
"desc": "Issuer Tracker",
"name": "Issuer Tracker",
"desc": "Issue Tracker",
"name": "Issue Tracker",
"order": 0,
"type": "url",
"used": true
@ -13368,6 +13374,16 @@
"model": "name.rolename",
"pk": "lead"
},
{
"fields": {
"desc": "",
"name": "Lead Maintainer",
"order": 0,
"used": true
},
"model": "name.rolename",
"pk": "leadmaintainer"
},
{
"fields": {
"desc": "",
@ -16778,7 +16794,7 @@
"fields": {
"command": "xym",
"switch": "--version",
"time": "2024-02-21T08:06:28.313Z",
"time": "2024-03-21T07:06:23.405Z",
"used": true,
"version": "xym 0.7.0"
},
@ -16789,7 +16805,7 @@
"fields": {
"command": "pyang",
"switch": "--version",
"time": "2024-02-21T08:06:28.663Z",
"time": "2024-03-21T07:06:23.755Z",
"used": true,
"version": "pyang 2.6.0"
},
@ -16800,7 +16816,7 @@
"fields": {
"command": "yanglint",
"switch": "--version",
"time": "2024-02-21T08:06:28.685Z",
"time": "2024-03-21T07:06:23.773Z",
"used": true,
"version": "yanglint SO 1.9.2"
},
@ -16811,9 +16827,9 @@
"fields": {
"command": "xml2rfc",
"switch": "--version",
"time": "2024-02-21T08:06:29.492Z",
"time": "2024-03-21T07:06:24.609Z",
"used": true,
"version": "xml2rfc 3.19.4"
"version": "xml2rfc 3.20.1"
},
"model": "utils.versioninfo",
"pk": 4

View file

@ -131,12 +131,15 @@ class AbstractReviewerQueuePolicy:
assignee_index = rotation_pks.index(assignee_person.pk)
skipped = rotation_pks[0:assignee_index]
skipped_settings = self.team.reviewersettings_set.filter(person__in=skipped) # list of PKs is valid here
changed = []
for ss in skipped_settings:
ss.skip_next = max(0, ss.skip_next - 1) # ensure we don't go negative
bulk_update_with_history(skipped_settings,
if ss.skip_next > 0:
ss.skip_next = max(0, ss.skip_next - 1) # ensure we don't go negative
ss._change_reason = "Skip count decremented"
changed.append(ss)
bulk_update_with_history(changed,
ReviewerSettings,
['skip_next'],
default_change_reason='skipped')
['skip_next'])
def _assignment_in_order(self, rotation_pks, assignee_person):
"""Is this an in-order assignment?"""
@ -262,12 +265,15 @@ class AbstractReviewerQueuePolicy:
def _clear_request_next_assignment(self, person):
s = self._reviewer_settings_for(person)
s.request_assignment_next = False
s.save()
if s.request_assignment_next:
s.request_assignment_next = False
s._change_reason = "Clearing request next assignment"
s.save()
def _add_skip(self, person):
s = self._reviewer_settings_for(person)
s.skip_next += 1
s._change_reason = "Incrementing skip count"
s.save()
def _reviewer_settings_for(self, person):
@ -484,6 +490,7 @@ class RotateAlphabeticallyReviewerQueuePolicy(AbstractReviewerQueuePolicy):
# Instead, the "assign me next" flag is set.
settings = self._reviewer_settings_for(reviewer_person)
settings.request_assignment_next = True
settings._change_reason = "Setting request next assignment"
settings.save()
def _update_skip_next(self, rotation_pks, assignee_person):
@ -523,20 +530,22 @@ class RotateAlphabeticallyReviewerQueuePolicy(AbstractReviewerQueuePolicy):
min_skip_next = min([rs.skip_next for rs in rotation_settings.values()])
next_reviewer_index = None
changed = []
for index, pk in enumerate(unfolded_rotation_pks):
rs = rotation_settings.get(pk)
if (rs is None) or (rs.skip_next == min_skip_next):
next_reviewer_index = index
break
else:
rs.skip_next = max(0, rs.skip_next - 1) # ensure never negative
if rs.skip_next > 0:
rs.skip_next = max(0, rs.skip_next - 1) # ensure never negative
rs._change_reason = "Skip count decremented"
changed.append(rs)
log.assertion('next_reviewer_index is not None') # some entry in the list must have the minimum value
bulk_update_with_history(rotation_settings.values(),
bulk_update_with_history(changed,
ReviewerSettings,
['skip_next'],
default_change_reason='skipped')
['skip_next'])
next_reviewer_pk = unfolded_rotation_pks[next_reviewer_index]
NextReviewerInTeam.objects.update_or_create(
@ -578,6 +587,7 @@ class LeastRecentlyUsedReviewerQueuePolicy(AbstractReviewerQueuePolicy):
# who rejected a review and no further action is needed.
settings = self._reviewer_settings_for(reviewer_person)
settings.request_assignment_next = True
settings._change_reason = "Setting request next assignment"
settings.save()

View file

@ -1,4 +1,4 @@
<h2>Activies Log</h2>
<h2>Activities Log</h2>
<div class="inline-related last-related">
<table class="full-width">
<thead>

View file

@ -1,33 +1,22 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import re
import email
import base64
import os
import pyzmail
from django.conf import settings
from django.urls import reverse as urlreverse
from django.core.exceptions import ValidationError
from django.contrib.sites.models import Site
from django.template.loader import render_to_string
from django.utils.encoding import force_str
import debug # pyflakes:ignore
from ietf.utils.log import log
from ietf.utils.mail import send_mail, send_mail_message
from ietf.doc.models import Document
from ietf.ipr.mail import utc_from_string
from ietf.person.models import Person
from ietf.message.models import Message, MessageAttachment
from ietf.message.models import Message
from ietf.utils.accesstoken import generate_access_token
from ietf.mailtrigger.utils import gather_address_lists, get_base_submission_message_address
from ietf.submit.models import SubmissionEmailEvent, Submission
from ietf.mailtrigger.utils import gather_address_lists
from ietf.submit.checkers import DraftIdnitsChecker
from ietf.utils.timezone import date_today
def send_submission_confirmation(request, submission, chair_notice=False):
@ -196,181 +185,3 @@ def announce_to_authors(request, submission):
'group': group},
cc=cc)
def get_reply_to():
"""Returns a new reply-to address for use with an outgoing message. This is an
address with "plus addressing" using a random string. Guaranteed to be unique"""
local,domain = get_base_submission_message_address().split('@')
while True:
rand = force_str(base64.urlsafe_b64encode(os.urandom(12)))
address = "{}+{}@{}".format(local,rand,domain)
q = Message.objects.filter(reply_to=address)
if not q:
return address
def process_response_email(msg):
"""Saves an incoming message. msg=string. Message "To" field is expected to
be in the format ietf-submit+[identifier]@ietf.org. Expect to find a message with
a matching value in the reply_to field, associated to a submission.
Create a Message object for the incoming message and associate it to
the original message via new SubmissionEvent"""
message = email.message_from_string(force_str(msg))
to = message.get('To')
# exit if this isn't a response we're interested in (with plus addressing)
local,domain = get_base_submission_message_address().split('@')
if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to):
return None
try:
to_message = Message.objects.get(reply_to=to)
except Message.DoesNotExist:
log('Error finding matching message ({})'.format(to))
return None
try:
submission = to_message.manualevents.first().submission
except:
log('Error processing message ({})'.format(to))
return None
if not submission:
log('Error processing message - no submission ({})'.format(to))
return None
parts = pyzmail.parse.get_mail_parts(message)
body=''
for part in parts:
if part.is_body == 'text/plain' and part.disposition == None:
payload, used_charset = pyzmail.decode_text(part.get_payload(), part.charset, None)
body = body + payload + '\n'
by = Person.objects.get(name="(System)")
msg = submit_message_from_message(message, body, by)
desc = "Email: received message - manual post - {}-{}".format(
submission.name,
submission.rev)
submission_email_event = SubmissionEmailEvent.objects.create(
submission = submission,
desc = desc,
msgtype = 'msgin',
by = by,
message = msg,
in_reply_to = to_message
)
save_submission_email_attachments(submission_email_event, parts)
log("Received submission email from %s" % msg.frm)
return msg
def add_submission_email(request, remote_ip, name, rev, submission_pk, message, by, msgtype):
"""Add email to submission history"""
#in_reply_to = form.cleaned_data['in_reply_to']
# create Message
parts = pyzmail.parse.get_mail_parts(message)
body=''
for part in parts:
if part.is_body == 'text/plain' and part.disposition == None:
payload, used_charset = pyzmail.decode_text(part.get_payload(), part.charset, None)
body = body + payload + '\n'
msg = submit_message_from_message(message, body, by)
if (submission_pk != None):
# Must exist - we're adding a message to an existing submission
submission = Submission.objects.get(pk=submission_pk)
else:
# Must not exist
submissions = Submission.objects.filter(name=name,rev=rev).exclude(state_id='cancel')
if submissions.count() > 0:
raise ValidationError("Submission {} already exists".format(name))
# create Submission using the name
try:
submission = Submission.objects.create(
state_id="waiting-for-draft",
remote_ip=remote_ip,
name=name,
rev=rev,
title=name,
note="",
submission_date=date_today(),
replaces="",
)
from ietf.submit.utils import create_submission_event, docevent_from_submission
desc = "Submission created for rev {} in response to email".format(rev)
create_submission_event(request,
submission,
desc)
docevent_from_submission(submission, desc)
except Exception as e:
log("Exception: %s\n" % e)
raise
if msgtype == 'msgin':
rs = "Received"
else:
rs = "Sent"
desc = "{} message - manual post - {}-{}".format(rs, name, rev)
submission_email_event = SubmissionEmailEvent.objects.create(
desc = desc,
submission = submission,
msgtype = msgtype,
by = by,
message = msg)
#in_reply_to = in_reply_to
save_submission_email_attachments(submission_email_event, parts)
return submission, submission_email_event
def submit_message_from_message(message,body,by=None):
"""Returns a ietf.message.models.Message. msg=email.Message
A copy of mail.message_from_message with different body handling
"""
if not by:
by = Person.objects.get(name="(System)")
msg = Message.objects.create(
by = by,
subject = message.get('subject',''),
frm = message.get('from',''),
to = message.get('to',''),
cc = message.get('cc',''),
bcc = message.get('bcc',''),
reply_to = message.get('reply_to',''),
body = body,
time = utc_from_string(message.get('date', '')),
content_type = message.get('content_type', 'text/plain'),
)
return msg
def save_submission_email_attachments(submission_email_event, parts):
for part in parts:
if part.disposition != 'attachment':
continue
if part.type == 'text/plain':
payload, used_charset = pyzmail.decode_text(part.get_payload(),
part.charset,
None)
encoding = ""
else:
# Need a better approach - for the moment we'll just handle these
# and encode as base64
payload = base64.b64encode(part.get_payload())
encoding = "base64"
#name = submission_email_event.submission.name
MessageAttachment.objects.create(message = submission_email_event.message,
content_type = part.type,
encoding = encoding,
filename=part.filename,
body=payload)

View file

@ -1,32 +0,0 @@
# Copyright The IETF Trust 2016-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import io
import sys
from django.core.management.base import BaseCommand, CommandError
from ietf.submit.mail import process_response_email
import debug # pyflakes:ignore
class Command(BaseCommand):
help = ("Process incoming manual post email responses")
def add_arguments(self, parser):
parser.add_argument('--email-file', dest='email', help='File containing email (default: stdin)')
def handle(self, *args, **options):
email = options.get('email', None)
msg = None
if not email:
msg = sys.stdin.read()
else:
msg = io.open(email, "r").read()
try:
process_response_email(msg)
except ValueError as e:
raise CommandError(e)

View file

@ -42,17 +42,15 @@ from ietf.group.models import Group
from ietf.group.utils import setup_default_community_list_for_group
from ietf.meeting.models import Meeting
from ietf.meeting.factories import MeetingFactory
from ietf.message.models import Message
from ietf.name.models import FormalLanguageName
from ietf.person.models import Person
from ietf.person.factories import UserFactory, PersonFactory, EmailFactory
from ietf.submit.factories import SubmissionFactory, SubmissionExtResourceFactory
from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm
from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
from ietf.submit.mail import add_submission_email, process_response_email
from ietf.submit.tasks import cancel_stale_submissions, process_and_accept_uploaded_submission_task
from ietf.utils.accesstoken import generate_access_token
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
from ietf.utils.mail import outbox, get_payload_text
from ietf.utils.models import VersionInfo
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
from ietf.utils.timezone import date_today
@ -198,6 +196,28 @@ def create_draft_submission_with_rev_mismatch(rev='01'):
return draft, sub
class ManualSubmissionTests(TestCase):
def test_manualpost_view(self):
submission = SubmissionFactory(state_id="manual")
url = urlreverse("ietf.submit.views.manualpost")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn(
urlreverse(
"ietf.submit.views.submission_status",
kwargs=dict(submission_id=submission.pk)
),
q("#manual.submissions td a").attr("href")
)
self.assertIn(
submission.name,
q("#manual.submissions td a").text()
)
def test_manualpost_cancel(self):
pass
class SubmitTests(BaseSubmitTestCase):
def setUp(self):
super().setUp()
@ -2293,451 +2313,6 @@ class ApprovalsTestCase(BaseSubmitTestCase):
self.assertEqual(len(Preapproval.objects.filter(name=preapproval.name)), 0)
class ManualPostsTestCase(BaseSubmitTestCase):
def test_manual_posts(self):
GroupFactory(acronym='mars')
url = urlreverse('ietf.submit.views.manualpost')
# Secretariat has access
self.client.login(username="secretary", password="secretary+password")
Submission.objects.create(name="draft-ietf-mars-foo",
group=Group.objects.get(acronym="mars"),
submission_date=date_today(),
state_id="manual")
Submission.objects.create(name="draft-ietf-mars-bar",
group=Group.objects.get(acronym="mars"),
submission_date=date_today(),
rev="00",
state_id="grp-appr")
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.submissions a:contains("draft-ietf-mars-foo")')), 1)
self.assertEqual(len(q('.submissions a:contains("draft-ietf-mars-bar")')), 0)
def test_waiting_for_draft(self):
message_string = """To: somebody@ietf.org
From: joe@test.com
Date: {}
Subject: test submission via email
Please submit my draft at http://test.com/mydraft.txt
Thank you
""".format(timezone.now().ctime())
message = email.message_from_string(force_str(message_string))
submission, submission_email_event = (
add_submission_email(request=None,
remote_ip ="192.168.0.1",
name = "draft-my-new-draft",
rev='00',
submission_pk=None,
message = message,
by = Person.objects.get(name="(System)"),
msgtype = "msgin") )
url = urlreverse('ietf.submit.views.manualpost')
# Secretariat has access
self.client.login(username="secretary", password="secretary+password")
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.waiting-for-draft a:contains("draft-my-new-draft")')), 1)
# Same name should raise an error
with self.assertRaises(Exception):
add_submission_email(request=None,
remote_ip ="192.168.0.1",
name = "draft-my-new-draft",
rev='00',
submission_pk=None,
message = message,
by = Person.objects.get(name="(System)"),
msgtype = "msgin")
# Cancel this one
r = self.client.post(urlreverse("ietf.submit.views.cancel_waiting_for_draft"), {
"submission_id": submission.pk,
"access_token": submission.access_token(),
})
self.assertEqual(r.status_code, 302)
url = r["Location"]
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.waiting-for-draft a:contains("draft-my-new-draft")')), 0)
# Should now be able to add it again
submission, submission_email_event = (
add_submission_email(request=None,
remote_ip ="192.168.0.1",
name = "draft-my-new-draft",
rev='00',
submission_pk=None,
message = message,
by = Person.objects.get(name="(System)"),
msgtype = "msgin") )
def test_waiting_for_draft_with_attachment(self):
frm = "joe@test.com"
message_string = """To: somebody@ietf.org
From: {}
Date: {}
Subject: A very important message with a small attachment
Content-Type: multipart/mixed; boundary="------------090908050800030909090207"
This is a multi-part message in MIME format.
--------------090908050800030909090207
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
The message body will probably say something about the attached document
--------------090908050800030909090207
Content-Type: text/plain; charset=UTF-8; name="attach.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="attach.txt"
QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs
ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg==
--------------090908050800030909090207--
""".format(frm, timezone.now().ctime())
message = email.message_from_string(force_str(message_string))
submission, submission_email_event = (
add_submission_email(request=None,
remote_ip ="192.168.0.1",
name = "draft-my-new-draft",
rev='00',
submission_pk=None,
message = message,
by = Person.objects.get(name="(System)"),
msgtype = "msgin") )
manualpost_page_url = urlreverse('ietf.submit.views.manualpost')
# Secretariat has access
self.client.login(username="secretary", password="secretary+password")
self.check_manualpost_page(submission=submission,
submission_email_event=submission_email_event,
the_url=manualpost_page_url,
submission_name_fragment='draft-my-new-draft',
frm=frm,
is_secretariat=True)
# Try the status page with no credentials
self.client.logout()
self.check_manualpost_page(submission=submission,
submission_email_event=submission_email_event,
the_url=manualpost_page_url,
submission_name_fragment='draft-my-new-draft',
frm=frm,
is_secretariat=False)
# Post another message to this submission using the link
message_string = """To: somebody@ietf.org
From: joe@test.com
Date: {}
Subject: A new submission message with a small attachment
Content-Type: multipart/mixed; boundary="------------090908050800030909090207"
This is a multi-part message in MIME format.
--------------090908050800030909090207
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
The message body will probably say something more about the attached document
--------------090908050800030909090207
Content-Type: text/plain; charset=UTF-8; name="attach.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="attachment.txt"
QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs
ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg==
--------------090908050800030909090207--
""".format(timezone.now().ctime())
# Back to secretariat
self.client.login(username="secretary", password="secretary+password")
r, q = self.request_and_parse(manualpost_page_url)
url = self.get_href(q, "a#new-submission-email:contains('New submission from email')")
# Get the form
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
#self.assertEqual(len(q('input[name=edit-title]')), 1)
# Post the new message
r = self.client.post(url, {
"name": "draft-my-next-new-draft-00",
"direction": "incoming",
"message": message_string,
})
if r.status_code != 302:
q = PyQuery(r.content)
print(q)
self.assertEqual(r.status_code, 302)
#self.check_manualpost_page(submission, submission_email_event,
# url, 'draft-my-next-new-draft'
# 'Another very important message',
# true)
def check_manualpost_page(self, submission, submission_email_event,
the_url, submission_name_fragment,
frm,
is_secretariat):
# get the page listing manual posts
r, q = self.request_and_parse(the_url)
selector = "#waiting-for-draft a#add-submission-email%s:contains('Add email')" % submission.pk
if is_secretariat:
# Can add an email to the submission
add_email_url = self.get_href(q, selector)
else:
# No add email button button
self.assertEqual(len(q(selector)), 0)
# Find the link for our submission in those awaiting drafts
submission_url = self.get_href(q, "#waiting-for-draft a#aw{}:contains('{}')".
format(submission.pk, submission_name_fragment))
# Follow the link to the status page for this submission
r, q = self.request_and_parse(submission_url)
selector = "#history a#reply%s:contains('Reply')" % submission.pk
if is_secretariat:
# check that reply button is visible and get the form
reply_url = self.get_href(q, selector)
# Get the form
r = self.client.get(reply_url)
self.assertEqual(r.status_code, 200)
reply_q = PyQuery(r.content)
self.assertEqual(len(reply_q('input[name=to]')), 1)
else:
# No reply button
self.assertEqual(len(q(selector)), 0)
if is_secretariat:
# Now try to send an email using the send email link
selector = "a#send%s:contains('Send Email')" % submission.pk
send_url = self.get_href(q, selector)
self.do_submission_email(the_url = send_url,
to = frm,
body = "A new message")
# print q
# print submission.pk
# print submission_email_event.pk
# Find the link for our message in the list
url = self.get_href(q, "#aw{}-{}:contains('{}')".format(submission.pk,
submission_email_event.message.pk,
"Received message - manual post"))
# Page displaying message details
r, q = self.request_and_parse(url)
if is_secretariat:
# check that reply button is visible
reply_href = self.get_href(q, "a#reply%s:contains('Reply')" % submission.pk)
else:
# No reply button
self.assertEqual(len(q(selector)), 0)
reply_href = None
# check that attachment link is visible
url = self.get_href(q, "#email-details a#attach{}:contains('attach.txt')".format(submission.pk))
# Fetch the attachment
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Attempt a reply if we can
if reply_href == None:
return
self.do_submission_email(the_url = reply_href,
to = frm,
body = "A reply to the message")
# try adding an email to the submission
# Use the add email link from the manual post listing page
if is_secretariat:
# Can add an email to the submission
# add_email_url set previously
r = self.client.get(add_email_url)
self.assertEqual(r.status_code, 200)
add_email_q = PyQuery(r.content)
self.assertEqual(len(add_email_q('input[name=submission_pk]')), 1)
# Add a simple email
new_message_string = """To: somebody@ietf.org
From: joe@test.com
Date: {}
Subject: Another message
About my submission
Thank you
""".format(timezone.now().ctime())
r = self.client.post(add_email_url, {
"name": "{}-{}".format(submission.name, submission.rev),
"direction": "incoming",
"submission_pk": submission.pk,
"message": new_message_string,
})
if r.status_code != 302:
q = PyQuery(r.content)
print(q)
self.assertEqual(r.status_code, 302)
def request_and_parse(self, url):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
return r, PyQuery(r.content)
def get_href(self, q, query):
link = q(query)
self.assertEqual(len(link), 1)
return PyQuery(link[0]).attr('href')
def do_submission_email(self, the_url, to, body):
# check the page
r = self.client.get(the_url)
q = PyQuery(r.content)
post_button = q('[type=submit]:contains("Send email")')
self.assertEqual(len(post_button), 1)
subject = post_button.parents("form").find('input[name="subject"]').val()
frm = post_button.parents("form").find('input[name="frm"]').val()
cc = post_button.parents("form").find('input[name="cc"]').val()
reply_to = post_button.parents("form").find('input[name="reply_to"]').val()
empty_outbox()
# post submitter info
r = self.client.post(the_url, {
"subject": subject,
"frm": frm,
"to": to,
"cc": cc,
"reply_to": reply_to,
"body": body,
})
self.assertEqual(r.status_code, 302)
self.assertEqual(len(outbox), 1)
outmsg = outbox[0]
self.assertTrue(to in outmsg['To'])
reply_to = outmsg['Reply-To']
self.assertIsNotNone(reply_to, "Expected Reply-To")
# Build a reply
message_string = """To: {}
From: {}
Date: {}
Subject: test
""".format(reply_to, to, timezone.now().ctime())
result = process_response_email(message_string)
self.assertIsInstance(result, Message)
return r
def do_submission(self, name, rev, group=None, formats=["txt",]):
# We're not testing the submission process - just the submission status
# get
url = urlreverse('ietf.submit.views.upload_submission')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('input[type=file][name=txt]')), 1)
self.assertEqual(len(q('input[type=file][name=xml]')), 1)
# submit
files = {}
for format in formats:
files[format], author = submission_file(f'{name}-{rev}', f'{name}-{rev}.{format}', group, "test_submission.%s" % format)
r = self.post_to_upload_submission(url, files)
if r.status_code != 302:
q = PyQuery(r.content)
print(q('div.invalid-feedback span.form-text div').text())
self.assertEqual(r.status_code, 302)
status_url = r["Location"]
for format in formats:
self.assertTrue(os.path.exists(os.path.join(self.staging_dir, "%s-%s.%s" % (name, rev, format))))
self.assertEqual(Submission.objects.filter(name=name).count(), 1)
submission = Submission.objects.get(name=name)
self.assertTrue(all([ c.passed!=False for c in submission.checks.all() ]))
self.assertEqual(len(submission.authors), 1)
author = submission.authors[0]
self.assertEqual(author["name"], "Author Name")
self.assertEqual(author["email"], "author@example.com")
return status_url
def supply_extra_metadata(self, name, status_url, submitter_name, submitter_email):
# check the page
r = self.client.get(status_url)
q = PyQuery(r.content)
post_button = q('[type=submit]:contains("Post")')
self.assertEqual(len(post_button), 1)
action = post_button.parents("form").find('input[type=hidden][name="action"]').val()
# post submitter info
r = self.client.post(status_url, {
"action": action,
"submitter-name": submitter_name,
"submitter-email": submitter_email,
"approvals_received": True,
})
if r.status_code == 302:
submission = Submission.objects.get(name=name)
self.assertEqual(submission.submitter, email.utils.formataddr((submitter_name, submitter_email)))
return r
# Transaction.on_commit() requires use of TransactionTestCase, but that has a performance penalty. Replace it
# with a no-op for testing purposes.

View file

@ -17,15 +17,6 @@ urlpatterns = [
url(r'^approvals/cancelpreapproval/(?P<preapproval_id>[a-f\d]+)/$', views.cancel_preapproval),
url(r'^manualpost/$', views.manualpost),
url(r'^manualpost/addemail$', views.add_manualpost_email),
url(r'^manualpost/addemail/(?P<submission_id>\d+)/(?P<access_token>[a-f\d]*)/$', views.add_manualpost_email),
url(r'^manualpost/attachment/(?P<submission_id>\d+)/(?P<message_id>\d+)/(?P<filename>.*)$', views.show_submission_email_attachment),
url(r'^manualpost/cancel$', views.cancel_waiting_for_draft),
url(r'^manualpost/email/(?P<submission_id>\d+)/(?P<message_id>\d+)/$', views.show_submission_email_message),
url(r'^manualpost/email/(?P<submission_id>\d+)/(?P<message_id>\d+)/(?P<access_token>[a-f\d]*)/$', views.show_submission_email_message),
url(r'^manualpost/replyemail/(?P<submission_id>\d+)/(?P<message_id>\d+)/$', views.send_submission_email),
url(r'^manualpost/sendemail/(?P<submission_id>\d+)/$', views.send_submission_email),
# proof-of-concept for celery async tasks
url(r'^async-poke/?$', views.async_poke_test),
]
]

View file

@ -3,7 +3,6 @@
import re
import base64
import datetime
from typing import Optional, cast # pyflakes:ignore
@ -22,31 +21,30 @@ from django.views.decorators.csrf import csrf_exempt
import debug # pyflakes:ignore
from ietf.doc.models import Document, AddedMessageEvent
from ietf.doc.models import Document
from ietf.doc.forms import ExtResourceForm
from ietf.group.models import Group
from ietf.group.utils import group_features_group_filter
from ietf.ietfauth.utils import has_role, role_required
from ietf.mailtrigger.utils import gather_address_lists
from ietf.message.models import Message, MessageAttachment
from ietf.person.models import Email
from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm,
PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm,
PreapprovalForm, ReplacesForm,
DeprecatedSubmissionAutoUploadForm, SubmissionManualUploadForm)
from ietf.submit.mail import send_full_url, send_manual_post_request, add_submission_email, get_reply_to
from ietf.submit.mail import send_full_url, send_manual_post_request
from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource,
DraftSubmissionStateName, SubmissionEmailEvent )
DraftSubmissionStateName )
from ietf.submit.tasks import process_uploaded_submission_task, process_and_accept_uploaded_submission_task, poke
from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user,
recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission,
post_submission, cancel_submission, rename_submission_files, remove_submission_files, get_draft_meta,
get_submission, fill_in_submission, apply_checkers, save_files, clear_existing_files,
check_submission_revision_consistency, accept_submission, accept_submission_requires_group_approval,
accept_submission_requires_prev_auth_approval, update_submission_external_resources, remote_ip )
accept_submission_requires_prev_auth_approval, update_submission_external_resources)
from ietf.stats.utils import clean_country_name
from ietf.utils.accesstoken import generate_access_token
from ietf.utils.log import log
from ietf.utils.mail import parseaddr, send_mail_message
from ietf.utils.mail import parseaddr
from ietf.utils.response import permission_denied
from ietf.utils.timezone import date_today
@ -780,243 +778,14 @@ def manualpost(request):
s.passes_checks = all([ c.passed!=False for c in s.checks.all() ])
s.errors = validate_submission(s)
waiting_for_draft = Submission.objects.filter(state_id = "waiting-for-draft").distinct()
return render(request, 'submit/manual_post.html',
{'manual': manual,
'selected': 'manual_posts',
'waiting_for_draft': waiting_for_draft})
def cancel_waiting_for_draft(request):
if request.method == 'POST':
can_cancel = has_role(request.user, "Secretariat")
if not can_cancel:
permission_denied(request, 'You do not have permission to perform this action.')
submission_id = request.POST.get('submission_id', '')
access_token = request.POST.get('access_token', '')
submission = get_submission_or_404(submission_id, access_token = access_token)
cancel_submission(submission)
create_submission_event(request, submission, "Cancelled submission")
if (submission.rev != "00"):
# Add a doc event
docevent_from_submission(submission, "Cancelled submission for rev {}".format(submission.rev))
return redirect("ietf.submit.views.manualpost")
@role_required('Secretariat',)
def add_manualpost_email(request, submission_id=None, access_token=None):
"""Add email to submission history"""
if request.method == 'POST':
try:
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return redirect("submit/manual_post.html")
form = SubmissionEmailForm(request.POST)
if form.is_valid():
submission_pk = form.cleaned_data['submission_pk']
message = form.cleaned_data['message']
#in_reply_to = form.cleaned_data['in_reply_to']
# create Message
if form.cleaned_data['direction'] == 'incoming':
msgtype = 'msgin'
else:
msgtype = 'msgout'
submission, submission_email_event = (
add_submission_email(request=request,
remote_ip=remote_ip(request),
name = form.draft_name,
rev=form.revision,
submission_pk = submission_pk,
message = message,
by = request.user.person,
msgtype = msgtype) )
messages.success(request, 'Email added.')
try:
draft = Document.objects.get(name=submission.name)
except Document.DoesNotExist:
# Assume this is revision 00 - we'll do this later
draft = None
if (draft != None):
e = AddedMessageEvent(type="added_message", doc=draft)
e.message = submission_email_event.submissionemailevent.message
e.msgtype = submission_email_event.submissionemailevent.msgtype
e.in_reply_to = submission_email_event.submissionemailevent.in_reply_to
e.by = request.user.person
e.desc = submission_email_event.desc
e.time = submission_email_event.time
e.save()
return redirect("ietf.submit.views.manualpost")
except ValidationError as e:
form = SubmissionEmailForm(request.POST)
form._errors = {}
form._errors["__all__"] = form.error_class(["There was a failure uploading your message. (%s)" % e.message])
else:
initial = {
return render(
request,
'submit/manual_post.html',
{
'manual': manual,
'selected': 'manual_posts'
}
if (submission_id != None):
submission = get_submission_or_404(submission_id, access_token)
initial['name'] = "{}-{}".format(submission.name, submission.rev)
initial['direction'] = 'incoming'
initial['submission_pk'] = submission.pk
else:
initial['direction'] = 'incoming'
form = SubmissionEmailForm(initial=initial)
return render(request, 'submit/add_submit_email.html',dict(form=form))
@role_required('Secretariat',)
def send_submission_email(request, submission_id, message_id=None):
"""Send an email related to a submission"""
submission = get_submission_or_404(submission_id, access_token = None)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return redirect('ietf.submit.views.submission_status',
submission_id=submission.id,
access_token=submission.access_token())
form = MessageModelForm(request.POST)
if form.is_valid():
# create Message
msg = Message.objects.create(
by = request.user.person,
subject = form.cleaned_data['subject'],
frm = form.cleaned_data['frm'],
to = form.cleaned_data['to'],
cc = form.cleaned_data['cc'],
bcc = form.cleaned_data['bcc'],
reply_to = form.cleaned_data['reply_to'],
body = form.cleaned_data['body']
)
in_reply_to_id = form.cleaned_data['in_reply_to_id']
in_reply_to = None
rp = ""
if in_reply_to_id:
rp = " reply"
try:
in_reply_to = Message.objects.get(id=in_reply_to_id)
except Message.DoesNotExist:
log("Unable to retrieve in_reply_to message: %s" % in_reply_to_id)
desc = "Sent message {} - manual post - {}-{}".format(rp,
submission.name,
submission.rev)
SubmissionEmailEvent.objects.create(
submission = submission,
desc = desc,
msgtype = 'msgout',
by = request.user.person,
message = msg,
in_reply_to = in_reply_to)
# send email
send_mail_message(None,msg)
messages.success(request, 'Email sent.')
return redirect('ietf.submit.views.submission_status',
submission_id=submission.id,
access_token=submission.access_token())
else:
reply_to = get_reply_to()
msg = None
if not message_id:
addrs = gather_address_lists('sub_confirmation_requested',submission=submission).as_strings(compact=False)
to_email = addrs.to
cc = addrs.cc
subject = 'Regarding {}'.format(submission.name)
else:
try:
submitEmail = SubmissionEmailEvent.objects.get(id=message_id)
msg = submitEmail.message
if msg:
to_email = msg.frm
cc = msg.cc
subject = 'Re:{}'.format(msg.subject)
else:
to_email = None
cc = None
subject = 'Regarding {}'.format(submission.name)
except Message.DoesNotExist:
to_email = None
cc = None
subject = 'Regarding {}'.format(submission.name)
initial = {
'to': to_email,
'cc': cc,
'frm': settings.IDSUBMIT_FROM_EMAIL,
'subject': subject,
'reply_to': reply_to,
}
if msg:
initial['in_reply_to_id'] = msg.id
form = MessageModelForm(initial=initial)
return render(request, "submit/email.html", {
'submission': submission,
'access_token': submission.access_token(),
'form':form})
def show_submission_email_message(request, submission_id, message_id, access_token=None):
submission = get_submission_or_404(submission_id, access_token)
submitEmail = get_object_or_404(SubmissionEmailEvent, pk=message_id)
attachments = submitEmail.message.messageattachment_set.all()
return render(request, 'submit/submission_email.html',
{'submission': submission,
'message': submitEmail,
'attachments': attachments})
def show_submission_email_attachment(request, submission_id, message_id, filename, access_token=None):
get_submission_or_404(submission_id, access_token)
message = get_object_or_404(SubmissionEmailEvent, pk=message_id)
attach = get_object_or_404(MessageAttachment,
message=message.message,
filename=filename)
if attach.encoding == "base64":
body = base64.b64decode(attach.body)
else:
body = attach.body.encode('utf-8')
if attach.content_type is None:
content_type='text/plain'
else:
content_type=attach.content_type
response = HttpResponse(body, content_type=content_type)
response['Content-Disposition'] = 'attachment; filename=%s' % attach.filename
response['Content-Length'] = len(body)
return response
)
def get_submission_or_404(submission_id, access_token=None):

View file

@ -802,6 +802,10 @@ def post_approved_draft(url, name):
the data from the Datatracker and start processing it. Returns
response and error (empty string if no error)."""
if settings.SERVER_MODE != "production":
log(f"In production, would have posted RFC-Editor notification of approved I-D '{name}' to '{url}'")
return "", ""
# HTTP basic auth
username = "dtracksync"
password = settings.RFC_EDITOR_SYNC_PASSWORD

View file

@ -597,6 +597,29 @@ class RFCSyncTests(TestCase):
auth48_docurl = draft.documenturl_set.filter(tag_id='auth48').first()
self.assertIsNone(auth48_docurl)
def test_post_approved_draft_in_production_only(self):
self.requests_mock.post("https://rfceditor.example.com/", status_code=200, text="OK")
# be careful playing with SERVER_MODE!
with override_settings(SERVER_MODE="test"):
self.assertEqual(
rfceditor.post_approved_draft("https://rfceditor.example.com/", "some-draft"),
("", "")
)
self.assertFalse(self.requests_mock.called)
with override_settings(SERVER_MODE="development"):
self.assertEqual(
rfceditor.post_approved_draft("https://rfceditor.example.com/", "some-draft"),
("", "")
)
self.assertFalse(self.requests_mock.called)
with override_settings(SERVER_MODE="production"):
self.assertEqual(
rfceditor.post_approved_draft("https://rfceditor.example.com/", "some-draft"),
("", "")
)
self.assertTrue(self.requests_mock.called)
class DiscrepanciesTests(TestCase):
def test_discrepancies(self):
@ -636,6 +659,7 @@ class DiscrepanciesTests(TestCase):
r = self.client.get(urlreverse("ietf.sync.views.discrepancies"))
self.assertContains(r, doc.name)
class RFCEditorUndoTests(TestCase):
def test_rfceditor_undo(self):
draft = WgDraftFactory()

View file

@ -267,7 +267,7 @@
</tr>
{% endif %}
</tbody>
{% if not doc.stream_id == 'iab' %}
{% if doc.stream_id != 'iab' and doc.stream_id != 'editorial' %}
<tbody class="meta border-top">
<tr>
<th scope="row">
@ -447,125 +447,127 @@
</tr>
</tbody>
{% endif %}
{% if can_edit_iana_state or iana_review_state or iana_experts_state or iana_experts_comment %}
<tbody class="meta border-top">
{% if iana_review_state or can_edit_iana_state %}
<tr>
<th scope="row">
IANA
</th>
<th scope="row">
<a href="{% url "ietf.doc.views_help.state_help" type="draft-iana-review" %}">
IANA review state
</a>
</th>
<td class="edit">
{% if can_edit_iana_state and not snapshot %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_draft.change_iana_state' name=doc.name state_type="iana-review" %}">
Edit
</a>
{% endif %}
</td>
<td class="{% if iana_review_state|slugify|slice:7 != "iana-ok" %}text-danger{% else %}text-success{% endif %}">
{% if not iana_review_state %}
<span class="text-body-secondary">
(None)
</span>
{% else %}
{{ iana_review_state }}
{% endif %}
</td>
</tr>
{% endif %}
{% if iana_action_state or can_edit_iana_state %}
<tr>
<th scope="row">
{% if not can_edit_iana_state and not iana_review_state %}IANA{% endif %}
</th>
<th scope="row">
<a href="{% url "ietf.doc.views_help.state_help" type="draft-iana-action" %}">
IANA action state
</a>
</th>
<td class="edit">
{% if can_edit_iana_state and not snapshot %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_draft.change_iana_state' name=doc.name state_type="iana-action" %}">
Edit
</a>
{% endif %}
</td>
<td>
{% if not iana_action_state %}
<span class="text-body-secondary">
(None)
</span>
{% else %}
{{ iana_action_state }}
{% endif %}
</td>
</tr>
{% endif %}
{% if iana_experts_state or can_edit_iana_state %}
<tr>
<th scope="row">
{% if not can_edit_iana_state and not iana_review_state and not iana_action_state %}IANA{% endif %}
</th>
<th scope="row">
<a href="{% url "ietf.doc.views_help.state_help" type="draft-iana-experts" %}">
IANA expert review state
</a>
</th>
<td class="edit">
{% if can_edit_iana_state and not snapshot %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_draft.change_iana_state' name=doc.name state_type="iana-experts" %}">
Edit
</a>
{% endif %}
</td>
<td class="{% if iana_experts_state|slugify|slice:"-2:" != "ok" %}text-danger{% else %}text-success{% endif %}">
{% if not iana_experts_state %}
<span class="text-body-secondary">
(None)
</span>
{% else %}
{{ iana_experts_state }}
{% endif %}
</td>
</tr>
{% endif %}
{% if iana_experts_comment or can_edit_iana_state %}
<tr>
<th scope="row">
{% if not can_edit_iana_state and not iana_review_state and not iana_action_state and not iana_experts_state %}
{% if doc.stream_id != 'editorial' %}
{% if can_edit_iana_state or iana_review_state or iana_experts_state or iana_experts_comment %}
<tbody class="meta border-top">
{% if iana_review_state or can_edit_iana_state %}
<tr>
<th scope="row">
IANA
{% endif %}
</th>
<th scope="row">
IANA expert review comments
</th>
<td class="edit">
{% if can_edit_iana_state and not snapshot %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_draft.add_iana_experts_comment' name=doc.name %}">
Edit
</th>
<th scope="row">
<a href="{% url "ietf.doc.views_help.state_help" type="draft-iana-review" %}">
IANA review state
</a>
{% endif %}
</td>
<td>
{% if not iana_experts_comment %}
<span class="text-body-secondary">
(None)
</span>
{% else %}
{{ iana_experts_comment }}
{% endif %}
</td>
</tr>
{% endif %}
</tbody>
</th>
<td class="edit">
{% if can_edit_iana_state and not snapshot %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_draft.change_iana_state' name=doc.name state_type="iana-review" %}">
Edit
</a>
{% endif %}
</td>
<td class="{% if iana_review_state|slugify|slice:7 != "iana-ok" %}text-danger{% else %}text-success{% endif %}">
{% if not iana_review_state %}
<span class="text-body-secondary">
(None)
</span>
{% else %}
{{ iana_review_state }}
{% endif %}
</td>
</tr>
{% endif %}
{% if iana_action_state or can_edit_iana_state %}
<tr>
<th scope="row">
{% if not can_edit_iana_state and not iana_review_state %}IANA{% endif %}
</th>
<th scope="row">
<a href="{% url "ietf.doc.views_help.state_help" type="draft-iana-action" %}">
IANA action state
</a>
</th>
<td class="edit">
{% if can_edit_iana_state and not snapshot %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_draft.change_iana_state' name=doc.name state_type="iana-action" %}">
Edit
</a>
{% endif %}
</td>
<td>
{% if not iana_action_state %}
<span class="text-body-secondary">
(None)
</span>
{% else %}
{{ iana_action_state }}
{% endif %}
</td>
</tr>
{% endif %}
{% if iana_experts_state or can_edit_iana_state %}
<tr>
<th scope="row">
{% if not can_edit_iana_state and not iana_review_state and not iana_action_state %}IANA{% endif %}
</th>
<th scope="row">
<a href="{% url "ietf.doc.views_help.state_help" type="draft-iana-experts" %}">
IANA expert review state
</a>
</th>
<td class="edit">
{% if can_edit_iana_state and not snapshot %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_draft.change_iana_state' name=doc.name state_type="iana-experts" %}">
Edit
</a>
{% endif %}
</td>
<td class="{% if iana_experts_state|slugify|slice:"-2:" != "ok" %}text-danger{% else %}text-success{% endif %}">
{% if not iana_experts_state %}
<span class="text-body-secondary">
(None)
</span>
{% else %}
{{ iana_experts_state }}
{% endif %}
</td>
</tr>
{% endif %}
{% if iana_experts_comment or can_edit_iana_state %}
<tr>
<th scope="row">
{% if not can_edit_iana_state and not iana_review_state and not iana_action_state and not iana_experts_state %}
IANA
{% endif %}
</th>
<th scope="row">
IANA expert review comments
</th>
<td class="edit">
{% if can_edit_iana_state and not snapshot %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_draft.add_iana_experts_comment' name=doc.name %}">
Edit
</a>
{% endif %}
</td>
<td>
{% if not iana_experts_comment %}
<span class="text-body-secondary">
(None)
</span>
{% else %}
{{ iana_experts_comment }}
{% endif %}
</td>
</tr>
{% endif %}
</tbody>
{% endif %}
{% endif %}
{% if rfc_editor_state %}
<tbody class="meta border-top">

View file

@ -23,12 +23,17 @@
{% if type.stategroups != None %}
<tr>
<td>
<a href="{% url 'ietf.help.views.state' doc=type.slug %}">{{ type.slug }}</a>
<a href="{% url 'ietf.doc.views_help.state_help' type=type.slug %}">{{ type.slug }}</a>
</td>
<td>
{% for group in type.stategroups %}
{# djlint:off #}
<a href="{% url 'ietf.help.views.state' doc=type.slug type=group %}">{{ group }}</a>{% if not forloop.last %},{% endif %}
{% if type.slug == "draft" %}
<a href="{% url 'ietf.doc.views_help.state_help' type=type.slug|add:'-'|add:group %}">
{% else %}
<a href="{% url 'ietf.help.views.state' doc=type.slug type=group %}">
{% endif %}
{{ group }}</a>{% if not forloop.last %},{% endif %}
{# djlint:on #}
{% endfor %}
</td>

View file

@ -105,11 +105,17 @@
{% if reviewersettings.history.all %}
<tbody>
{% for h in reviewersettings.history.all %}
{% if h|simple_history_delta_change_cnt > 0 or h.history_change_reason != "skipped" and h.history_change_reason %}
<tr>
<td>{{ h.history_date|date }}</td>
<td>{% person_link h.history_user.person %}</td>
<td>{{ h.history_change_reason }}</td>
<td>{% if h.history_change_reason != "skipped" and h.history_change_reason %} {{ h.history_change_reason }}<br> {% endif %}
{% for change in h|simple_history_delta_changes %}
{{ change.field }} changed from "{{change.old}}" to "{{change.new}}"<br>
{% endfor %}
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
{% endif %}

View file

@ -1,38 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin static ietf_filters textfilters %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
{% endblock %}
{% block title %}{{ type.label|cut:"state"|cut:"state" }} states{% endblock %}
{% block content %}
{% origin %}
<h1>{{ type.label|cut:"state"|cut:"State" }} states</h1>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="state">State</th>
<th scope="col" data-sort="desc">Description</th>
<th scope="col" data-sort="next">Next states</th>
</tr>
</thead>
<tbody>
{% for state in states %}
<tr>
<th scope="row">{{ state.name }}</th>
<td>{{ state.desc|urlize_ietf_docs|linkify }}</td>
<td>
<ul>
{% for s in state.next_states.all %}
<li>{{ s.name }}</li>
{% endfor %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block js %}
<script src="{% static "ietf/js/list.js" %}"></script>
{% endblock %}

View file

@ -5,10 +5,16 @@
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
{% endblock %}
{% block title %}Non-Working Group email lists{% endblock %}
{% block title %}Other (not WG) email lists{% endblock %}
{% block content %}
{% origin %}
<h1>Non-Working Group email lists</h1>
<h1>Other (not Working Group) email lists</h1>
<p>Guidelines for these lists, including how to request a
new one to be created, are at
<a href="https://www.ietf.org/how/lists/nonwglist-guidelines/">
https://www.ietf.org/how/lists/nonwglist-guidelines/
</a>
</p>
{% cache 900 nonwglisttable %}
<table class="table table-sm table-striped tablesorter">
<thead>

View file

@ -81,7 +81,7 @@
<small class="text-body-secondary">{{ form.nominee.email.person.name }} for {{ form.position.name }}</small>
</h2>
<p class="mt-3">
Provide feedback about {% person_link form.nominee.person %}
Provide feedback about {% person_link form.nominee.person with_email=False %}
for the <b>{{ form.position.name }}</b> position.
</p>
{% elif form.topic %}
@ -97,8 +97,8 @@
{% endif %}
<p>
This feedback will only be available to
<a href="{% url 'ietf.nomcom.views.year_index' year=year %}">NomCom {{ year }}</a>.
You may have the feedback mailed back to you by selecting the option below.
<a href="{% url 'ietf.nomcom.views.year_index' year=year %}">the current NomCom</a>.
You can have the feedback mailed back to you by selecting the option below.
</p>
<form class="float-start" id="feedbackform" method="post">
{% csrf_token %}

View file

@ -1,8 +1,8 @@
{% if email and email == "system@datatracker.ietf.org" or name and name == "(System)" %}<span class="text-body-secondary">(System)</span>{% else %}<span {% if class %}class="{{ class }}"
{% endif %}>{% if email or name %}<a {% if class %}class="text-reset"{% endif %}
title="{% if title %}{{ title }}{% else %}Datatracker profile of {{ name }}.{% endif %}"
title="{% if title %}{{ title }}{% else %}Datatracker profile of {{ name }}{% endif %}"
{% if email %} href="{% url 'ietf.person.views.profile' email_or_name=email %}" {% else %} href="{% url 'ietf.person.views.profile' email_or_name=name %}" {% endif %}>{{ name }}</a>{% if email and with_email %} <a {% if class %}class="text-reset"{% endif %}
href="mailto:{{ email|urlencode }}"
aria-label="Compose email to {{ email }}."
title="Compose email to {{ email }}.">
aria-label="Compose email to {{ email }}"
title="Compose email to {{ email }}">
<i class="bi bi-envelope"></i></a>{% endif %}{% else %}<span class="text-body-secondary">(None)</span>{% endif %}</span>{% endif %}

View file

@ -2,7 +2,7 @@
{% load origin static person_filters %}
<div class="card shadow-sm mb-3 text-center photo">
{% if name or email %}
<a title="Datatracker profile of {{ person.name }}."
<a title="Datatracker profile of {{ person.name }}"
href="{% if name %}{% url 'ietf.person.views.profile' email_or_name=name %}{% else %}{% url 'ietf.person.views.profile' email_or_name=email %}{% endif %}">
{% endif %}
{% if person.photo_thumb %}

View file

@ -1,32 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load django_bootstrap5 %}
{% block title %}
{% if submission == None %}
Add new submission request email
{% else %}
Add submission request email to {{ submission.name }}
{% endif %}
{% endblock %}
{% block content %}
{% origin %}
<h1>Add email</h1>
<p class="my-3">
{% if submission == None %}
A new submission request will be created for the given name and revision. The
name must take the form <code>draft-xxx-nn</code> where <code>xxx</code> is lowercase letters, digits or dashes
and <code>nn</code> is the revision number, <code>00</code> for the initial revision. For example,
<code>draft-my-spec-00</code>.
{% else %}
The email will be added to the submission history for {{ submission.name }}.
{% endif %}
</p>
<form class="add-email my-3" method="post">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Add Email</button>
<a class="btn btn-secondary float-end"
href="{% url "ietf.submit.views.manualpost" %}">Back</a>
</form>
{% endblock %}

View file

@ -1,25 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load django_bootstrap5 %}
{% block title %}Email related to{{ submission.name }}{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'ietf/css/datepicker.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>
Email related to
<br>
<small class="text-body-secondary">{{ submission.name }}</small>
</h1>
<form method="post" class="show-required my-3">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Send email</button>
</form>
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/datepicker.js' %}"></script>
{% endblock %}

View file

@ -58,76 +58,6 @@
</tbody>
</table>
{% endif %}
<h2 class="mt-3" id="id_upload">Submissions awaiting Internet-Draft upload</h2>
{% if not waiting_for_draft %}
<p class="alert alert-info my-3" id="no-waiting-for-draft">
There are no submissions awaiting Internet-Draft upload.
</p>
{% else %}
<table id="waiting-for-draft"
class="waiting-for-draft table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="name">Name</th>
<th scope="col" data-sort="rev">Rev</th>
<th scope="col" data-sort="date">Submitted</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for s in waiting_for_draft %}
<tr>
{% if user.is_authenticated %}
<td>
<a id="aw{{ s.pk }}"
href="{% url "ietf.submit.views.submission_status" submission_id=s.pk access_token=s.access_token %}">
{{ s.name }}
</a>
</td>
{% else %}
<td>
<a id="aw{{ s.pk }}"
href="{% url "ietf.submit.views.submission_status" submission_id=s.pk %}">{{ s.name }}</a>
</td>
{% endif %}
<td>{{ s.rev }}</td>
<td>{{ s.submission_date }}</td>
<td>
{% if user|has_role:"Secretariat" %}
<form id="cancel-submission"
action="/submit/awaitingdraft/cancel"
method="post">
{% csrf_token %}
<input type="hidden" name="submission_id" value="{{ s.pk }}">
<input type="hidden" name="access_token" value="{{ s.access_token }}">
<button class="btn btn-danger btn-sm float-end ms-1"
type="submit"
data-bs-toggle="tooltip"
title="Cancels the submission permanently.">
Cancel submission
</button>
</form>
{% endif %}
{% if user|has_role:"Secretariat" %}
<a id="add-submission-email{{ s.pk }}"
class="btn btn-primary btn-sm float-end ms-1"
href="{% url "ietf.submit.views.add_manualpost_email" submission_id=s.pk access_token=s.access_token %}">
Add email
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if user|has_role:"Secretariat" %}
<a id="new-submission-email"
class="btn btn-primary"
href="{% url "ietf.submit.views.add_manualpost_email" %}">
New submission from email
</a>
{% endif %}
{% endblock %}
{% block js %}
<script src="{% static "ietf/js/list.js" %}"></script>

View file

@ -1,61 +0,0 @@
{% extends "submit/submit_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin static textfilters ietf_filters %}
{% block title %}Submission email{% endblock %}
{% load ietf_filters %}
{% block submit_content %}
{% origin %}
<h2 id="approvals">Email for {{ submission.name }}</h2>
<dl id="email-details" class="emails row my-3">
<dt class="col-sm-2">
Uploaded
</dt>
<dd class="col-sm-10">
{{ message.time }}
</dd>
<dt class="col-sm-2">
Date
</dt>
<dd class="col-sm-10">
{{ message.message.time }}
</dd>
<dt class="col-sm-2">
From
</dt>
<dd class="col-sm-10">
{{ message.message.frm|linkify }}
</dd>
<dt class="col-sm-2">
Subject
</dt>
<dd class="col-sm-10">
{{ message.message.subject }}
</dd>
<dt class="col-sm-2">
Message
</dt>
<dd class="col-sm-10">
<pre>{{ message.message.body|urlize_ietf_docs|linkify|linebreaksbr }}</pre>
</dd>
<dt class="col-sm-2">
Attachment
</dt>
<dd class="col-sm-10">
{% for a in attachments %}
<a id="attach{{ submission.pk }}"
href="{% url "ietf.submit.views.show_submission_email_attachment" submission_id=submission.pk message_id=message.pk filename=a.filename %}">
{{ a.filename }}
</a>
<br>
{% endfor %}
</dd>
</dl>
{% if user|has_role:"Secretariat" %}
<a id="reply{{ submission.pk }}"
class="btn btn-primary"
href="{% url "ietf.submit.views.send_submission_email" submission_id=submission.pk message_id=message.pk %}"
title="Reply">
<i class="bi bi-envelope" aria-hidden="true"></i> Reply
</a>
{% endif %}
{% endblock %}

View file

@ -520,14 +520,6 @@
</button>
</form>
{% endif %}
{% if user|has_role:"Secretariat" %}
<a id="send{{ submission.pk }}"
class="btn btn-primary mt-3"
href="{% url "ietf.submit.views.send_submission_email" submission_id=submission.pk %}"
title="Email submitter">
<i class="bi bi-envelope" aria-hidden="true"></i> Send Email
</a>
{% endif %}
{% if show_send_full_url %}
<div class="alert alert-info my-3">
<p>
@ -584,39 +576,10 @@
{% endif %}
</td>
{% if e.desc|startswith:"Received message" or e.desc|startswith:"Sent message" %}
{% with m=e.submissionemailevent.message %}
{% if user.is_authenticated %}
<td>
{% if e.desc|startswith:"Received message" and user|has_role:"Secretariat" %}
<a id="reply{{ submission.pk }}"
class="btn btn-primary btn-sm"
href="{% url "ietf.submit.views.send_submission_email" submission_id=submission.pk message_id=e.submissionemailevent.pk %}"
title="Reply">
<i class="bi bi-envelope" aria-hidden="true"></i> Reply
</a>
{% endif %}
Email:
<a id="aw{{ submission.pk }}-{{ m.pk }}"
href="{% url "ietf.submit.views.show_submission_email_message" submission_id=submission.pk message_id=e.submissionemailevent.pk access_token=submission.access_token %}">
{{ e.desc }}
</a>
</td>
{% else %}
<td>
Email:
<a id="aw{{ submission.pk }}-{{ m.pk }}"
href="{% url "ietf.submit.views.show_submission_email_message" submission_id=submission.pk message_id=e.submissionemailevent.pk %}">
{{ e.desc }}
</a>
</td>
{% endif %}
{% endwith %}
{% else %}
<td>
{{ e.desc|urlize_ietf_docs|linkify }}
</td>
{% endif %}
<td>
{{ e.desc|urlize_ietf_docs|linkify }}
</td>
</tr>
{% endfor %}
</tbody>

View file

@ -4,7 +4,6 @@
import datetime
from decorator import decorator, decorate
from functools import wraps
from django.conf import settings
@ -20,25 +19,29 @@ from ietf.utils.test_runner import set_coverage_checking
from ietf.person.models import Person, PersonalApiKey, PersonApiKeyEvent
from ietf.utils import log
@decorator
def skip_coverage(f, *args, **kwargs):
if settings.TEST_CODE_COVERAGE_CHECKER:
set_coverage_checking(False)
result = f(*args, **kwargs)
set_coverage_checking(True)
return result
else:
return f(*args, **kwargs)
def skip_coverage(f):
@wraps(f)
def _wrapper(*args, **kwargs):
if settings.TEST_CODE_COVERAGE_CHECKER:
set_coverage_checking(False)
result = f(*args, **kwargs)
set_coverage_checking(True)
return result
else:
return f(*args, **kwargs)
return _wrapper
@decorator
def person_required(f, request, *args, **kwargs):
if not request.user.is_authenticated:
raise ValueError("The @person_required decorator should be called after @login_required.")
try:
request.user.person
except Person.DoesNotExist:
return render(request, 'registration/missing_person.html')
return f(request, *args, **kwargs)
def person_required(f):
@wraps(f)
def _wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
raise ValueError("The @person_required decorator should be called after @login_required.")
try:
request.user.person
except Person.DoesNotExist:
return render(request, 'registration/missing_person.html')
return f(request, *args, **kwargs)
return _wrapper
def require_api_key(f):
@ -90,29 +93,31 @@ def require_api_key(f):
return _wrapper
def _memoize(func, self, *args, **kwargs):
'''Memoize wrapper for instance methods. Use @lru_cache for functions.'''
if kwargs: # frozenset is used to ensure hashability
key = args, frozenset(list(kwargs.items()))
else:
key = args
# instance method, set up cache if needed
if not hasattr(self, '_cache'):
self._cache = {}
if not func in self._cache:
self._cache[func] = {}
#
cache = self._cache[func]
if key not in cache:
cache[key] = func(self, *args, **kwargs)
return cache[key]
def memoize(func):
@wraps(func)
def _memoize(self, *args, **kwargs):
'''Memoize wrapper for instance methods. Use @lru_cache for functions.'''
if kwargs: # frozenset is used to ensure hashability
key = args, frozenset(list(kwargs.items()))
else:
key = args
# instance method, set up cache if needed
if not hasattr(self, '_cache'):
self._cache = {}
if not func in self._cache:
self._cache[func] = {}
#
cache = self._cache[func]
if key not in cache:
cache[key] = func(self, *args, **kwargs)
return cache[key]
if not hasattr(func, '__class__'):
raise NotImplementedError("Use @lru_cache instead of memoize() for functions.")
# For methods, we want the cache on the object, not on the class, in order
# to not having to think about cache bloat and content becoming stale, so
# we cannot set up the cache here.
return decorate(func, _memoize)
return _memoize
def ignore_view_kwargs(*args):

View file

@ -15,7 +15,7 @@
"vueCompilerOptions": {
"target": 3,
"plugins": [
"@volar/vue-language-plugin-pug"
"@vue/language-plugin-pug"
]
}
}

View file

@ -21,8 +21,8 @@
"bootstrap": "5.3.3",
"bootstrap-icons": "1.11.3",
"browser-fs-access": "0.35.0",
"caniuse-lite": "1.0.30001597",
"d3": "7.8.5",
"caniuse-lite": "1.0.30001599",
"d3": "7.9.0",
"file-saver": "2.0.5",
"highcharts": "11.4.0",
"ical.js": "1.5.0",
@ -56,6 +56,7 @@
"@parcel/transformer-sass": "2.12.0",
"@rollup/pluginutils": "5.1.0",
"@vitejs/plugin-vue": "4.6.2",
"@vue/language-plugin-pug": "2.0.7",
"browserlist": "latest",
"c8": "9.1.0",
"eslint": "8.57.0",
@ -65,12 +66,12 @@
"eslint-plugin-n": "16.6.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.22.0",
"html-validate": "8.15.0",
"eslint-plugin-vue": "9.23.0",
"html-validate": "8.16.0",
"jquery-migrate": "3.4.1",
"parcel": "2.12.0",
"pug": "3.0.2",
"sass": "1.71.1",
"sass": "1.72.0",
"seedrandom": "3.0.5",
"vite": "4.5.2"
},

View file

@ -22,7 +22,7 @@
"eslint-plugin-n": "16.6.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "6.1.1",
"npm-check-updates": "16.14.15"
"npm-check-updates": "16.14.17"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -3804,9 +3804,9 @@
}
},
"node_modules/npm-check-updates": {
"version": "16.14.15",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz",
"integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==",
"version": "16.14.17",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.17.tgz",
"integrity": "sha512-ElnDdXKe60f8S6RhzFeaGuH2TFJmt2cU2HjLdowldabdm27nWFCxV2ebeP3xGbQkzp2+RPDQNdW9HqU1lcY8ag==",
"dev": true,
"dependencies": {
"chalk": "^5.3.0",
@ -8721,9 +8721,9 @@
}
},
"npm-check-updates": {
"version": "16.14.15",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz",
"integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==",
"version": "16.14.17",
"resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.17.tgz",
"integrity": "sha512-ElnDdXKe60f8S6RhzFeaGuH2TFJmt2cU2HjLdowldabdm27nWFCxV2ebeP3xGbQkzp2+RPDQNdW9HqU1lcY8ag==",
"dev": true,
"requires": {
"chalk": "^5.3.0",

View file

@ -14,7 +14,7 @@
"eslint-plugin-n": "16.6.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "6.1.1",
"npm-check-updates": "16.14.15"
"npm-check-updates": "16.14.17"
},
"dependencies": {
"@faker-js/faker": "8.4.1",

View file

@ -8,8 +8,6 @@ bleach>=6
types-bleach>=6
celery>=5.2.6
coverage>=4.5.4,<5.0 # Coverage 5.x moves from a json database to SQLite. Moving to 5.x will require substantial rewrites in ietf.utils.test_runner and ietf.release.views
decorator>=5.1.1
types-decorator>=5.1.1
defusedxml>=0.7.1 # for TastyPie when using xml; not a declared dependency
Django>4.2,<5
django-analytical>=3.1.0
@ -56,7 +54,7 @@ pyquery>=1.4.3
python-dateutil>=2.8.2
types-python-dateutil>=2.8.2
python-magic==0.4.18 # Versions beyond the yanked .19 and .20 introduce form failures
pymemcache>=4.0.0 # for django.core.cache.backends.memcached.PyMemcacheCache
pymemcache>=4.0.0 # for django.core.cache.backends.memcached.PyMemcacheCache
python-mimeparse>=1.6 # from TastyPie
pytz==2022.2.1 # Pinned as changes need to be vetted for their effect on Meeting fields
requests>=2.31.0

185
yarn.lock
View file

@ -2019,6 +2019,43 @@ __metadata:
languageName: node
linkType: hard
"@volar/language-core@npm:2.1.4":
version: 2.1.4
resolution: "@volar/language-core@npm:2.1.4"
dependencies:
"@volar/source-map": 2.1.4
checksum: 7430f651431ed00eb7489d48c0596f4653fe70da3c779acfaa5807051db4491c9e4e154e9f0de3c9d863a3b4b1194a517a75395ca9134ea2b1b8af5ff637b204
languageName: node
linkType: hard
"@volar/language-service@npm:~2.1.0":
version: 2.1.4
resolution: "@volar/language-service@npm:2.1.4"
dependencies:
"@volar/language-core": 2.1.4
vscode-languageserver-protocol: ^3.17.5
vscode-languageserver-textdocument: ^1.0.11
vscode-uri: ^3.0.8
checksum: 06cdcfacf0fab22cee652cab1ae1729628d7ebf68f5f9e791e19e3715b2a4775c0bd2ec2e7a9b0815d93f244d7a745f3ea41aa5084923b10e9258a5f54c1107b
languageName: node
linkType: hard
"@volar/source-map@npm:2.1.4, @volar/source-map@npm:~2.1.3":
version: 2.1.4
resolution: "@volar/source-map@npm:2.1.4"
dependencies:
muggle-string: ^0.4.0
checksum: e2f65bcfd667a02ee5cfe49e612b12e75c05fdaecf3b3590fdd7a0255dce7e51d09e8d4c390c2098ca7321cea219c16a8ea3f6c0f36ca9c0edff3975990b458b
languageName: node
linkType: hard
"@vscode/l10n@npm:^0.0.18":
version: 0.0.18
resolution: "@vscode/l10n@npm:0.0.18"
checksum: c33876cebdef0385359619200ecb5d7c46d7f9abffb80f9fab1f83abb5d6bfdb44cc6d792d1b1b9c736c729121274733bbdcd5d2d2eea0d157bdf662d521edef
languageName: node
linkType: hard
"@vue/compiler-core@npm:3.4.21":
version: 3.4.21
resolution: "@vue/compiler-core@npm:3.4.21"
@ -2083,6 +2120,16 @@ __metadata:
languageName: node
linkType: hard
"@vue/language-plugin-pug@npm:2.0.7":
version: 2.0.7
resolution: "@vue/language-plugin-pug@npm:2.0.7"
dependencies:
"@volar/source-map": ~2.1.3
volar-service-pug: 0.0.34
checksum: 11cc96eb5f240144e91b27fe06fcd48de4ef1e4c7fe666d1173b346ed64b7edfa922bd4eb2e512a91a0c6b907975afcaf69cfee4c91af11168590142b3aba4c3
languageName: node
linkType: hard
"@vue/reactivity@npm:3.4.21":
version: 3.4.21
resolution: "@vue/reactivity@npm:3.4.21"
@ -2637,10 +2684,10 @@ browserlist@latest:
languageName: node
linkType: hard
"caniuse-lite@npm:1.0.30001597":
version: 1.0.30001597
resolution: "caniuse-lite@npm:1.0.30001597"
checksum: ec6a2cf0fd49f37d16732e6595939fc80a125dcd188a950bc936c61b4ad53becc0fe51bf2d9a625415de7b1cb23bd835f220e8b68d8ab951a940edeea65476fd
"caniuse-lite@npm:1.0.30001599":
version: 1.0.30001599
resolution: "caniuse-lite@npm:1.0.30001599"
checksum: d7e619e2e723547b7311ba0ca5134d9cd55df548e93dbedcf8a6e4ec74c7db91969c4272fb1ab2fd94cddeac6a8176ebf05853eb06689d5e76bb97d979a214b0
languageName: node
linkType: hard
@ -3216,9 +3263,9 @@ browserlist@latest:
languageName: node
linkType: hard
"d3@npm:7.8.5":
version: 7.8.5
resolution: "d3@npm:7.8.5"
"d3@npm:7.9.0":
version: 7.9.0
resolution: "d3@npm:7.9.0"
dependencies:
d3-array: 3
d3-axis: 3
@ -3250,7 +3297,7 @@ browserlist@latest:
d3-timer: 3
d3-transition: 3
d3-zoom: 3
checksum: e407e79731f74d946a5eb8dec2f037b5a4ad33c294409b1d3531fdf7094de48adfe364974cb37e2396bdb81e23149d56d0ede716c004d6aebb52b3cc114cd15c
checksum: 1c0e9135f1fb78aa32b187fafc8b56ae6346102bd0e4e5e5a5339611a51e6038adbaa293fae373994228100eddd87320e930b1be922baeadc07c9fd43d26d99b
languageName: node
linkType: hard
@ -3938,9 +3985,9 @@ browserlist@latest:
languageName: node
linkType: hard
"eslint-plugin-vue@npm:9.22.0":
version: 9.22.0
resolution: "eslint-plugin-vue@npm:9.22.0"
"eslint-plugin-vue@npm:9.23.0":
version: 9.23.0
resolution: "eslint-plugin-vue@npm:9.23.0"
dependencies:
"@eslint-community/eslint-utils": ^4.4.0
natural-compare: ^1.4.0
@ -3951,7 +3998,7 @@ browserlist@latest:
xml-name-validator: ^4.0.0
peerDependencies:
eslint: ^6.2.0 || ^7.0.0 || ^8.0.0
checksum: 5f1e94b412567b8d0bb99c89ea1032d7801d1aa34344e61d82b71c926d3ced049861b484c31c641606923246d482972a45e690236e5479f46b9736d432545ecd
checksum: acb3a4dd27a815be37b63fc735c88bf9bfd12a5147639635eb6e473c7fdf47f69a78693ffeb67581ffd11e0016c134bb56a667193b958a189cbfc2df3e93160b
languageName: node
linkType: hard
@ -4667,9 +4714,9 @@ browserlist@latest:
languageName: node
linkType: hard
"html-validate@npm:8.15.0":
version: 8.15.0
resolution: "html-validate@npm:8.15.0"
"html-validate@npm:8.16.0":
version: 8.16.0
resolution: "html-validate@npm:8.16.0"
dependencies:
"@babel/code-frame": ^7.10.0
"@html-validate/stylish": ^4.1.0
@ -4698,7 +4745,7 @@ browserlist@latest:
optional: true
bin:
html-validate: bin/html-validate.js
checksum: 0af7685ca1302cbcbbaebae771b64048267aa3ce26fe279f99647b4febd03373e1fbb85cc3e5ba2b0986741eecb6a2b6f5a3143615b6d8407e28210199ec136a
checksum: 857b05ab87ae8b92d9d256dfc45def15275d6038bb7456afb00c6e23872acc4bc7bcc50be89ce82bad36f6c4026603106f57574f1058f3ae3f2e35fca0f34a15
languageName: node
linkType: hard
@ -5968,6 +6015,13 @@ browserlist@latest:
languageName: node
linkType: hard
"muggle-string@npm:^0.4.0":
version: 0.4.1
resolution: "muggle-string@npm:0.4.1"
checksum: 85fe1766d18d43cf22b6da7d047203a65b2e2b1ccfac505b699c2a459644f95ebb3c854a96db5be559eea0e213f6ee32b986b8c2f73c48e6c89e1fd829616532
languageName: node
linkType: hard
"murmurhash-js@npm:1.0.0":
version: 1.0.0
resolution: "murmurhash-js@npm:1.0.0"
@ -6982,13 +7036,14 @@ browserlist@latest:
"@rollup/pluginutils": 5.1.0
"@twuni/emojify": 1.0.2
"@vitejs/plugin-vue": 4.6.2
"@vue/language-plugin-pug": 2.0.7
bootstrap: 5.3.3
bootstrap-icons: 1.11.3
browser-fs-access: 0.35.0
browserlist: latest
c8: 9.1.0
caniuse-lite: 1.0.30001597
d3: 7.8.5
caniuse-lite: 1.0.30001599
d3: 7.9.0
eslint: 8.57.0
eslint-config-standard: 17.1.0
eslint-plugin-cypress: 2.15.1
@ -6996,10 +7051,10 @@ browserlist@latest:
eslint-plugin-n: 16.6.2
eslint-plugin-node: 11.1.0
eslint-plugin-promise: 6.1.1
eslint-plugin-vue: 9.22.0
eslint-plugin-vue: 9.23.0
file-saver: 2.0.5
highcharts: 11.4.0
html-validate: 8.15.0
html-validate: 8.16.0
ical.js: 1.5.0
jquery: 3.7.1
jquery-migrate: 3.4.1
@ -7017,7 +7072,7 @@ browserlist@latest:
pinia: 2.1.7
pinia-plugin-persist: 1.0.0
pug: 3.0.2
sass: 1.71.1
sass: 1.72.0
seedrandom: 3.0.5
select2: 4.1.0-rc.0
select2-bootstrap-5-theme: 1.3.0
@ -7093,16 +7148,16 @@ browserlist@latest:
languageName: node
linkType: hard
"sass@npm:1.71.1":
version: 1.71.1
resolution: "sass@npm:1.71.1"
"sass@npm:1.72.0":
version: 1.72.0
resolution: "sass@npm:1.72.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: 19c4939d3042eb9459d462bbd27b1f576fa18034e23c87ca0005b87effdee431c16503b5a785edcdcde1a76dfb804716d9ad42c85a78968ac3825d515e45cb53
checksum: f420079c7d51660b7256ee52463c1499ede36f7fd5c8ef50c687451777ad641509001454dea45244073cedd7c00e7a3bc1c362e55206ac6686171b994edb41e4
languageName: node
linkType: hard
@ -7868,6 +7923,36 @@ browserlist@latest:
languageName: node
linkType: hard
"volar-service-html@npm:0.0.34":
version: 0.0.34
resolution: "volar-service-html@npm:0.0.34"
dependencies:
vscode-html-languageservice: ^5.1.0
vscode-languageserver-textdocument: ^1.0.11
vscode-uri: ^3.0.8
peerDependencies:
"@volar/language-service": ~2.1.0
peerDependenciesMeta:
"@volar/language-service":
optional: true
checksum: 83b50cd805680c77b5632e9534b23cddb85bf7e0cd425624d474981d173ddf07a66fcce6348f675c9d5c2551df9ae1e58206c2ed1c32052f8a70940fb7f5fe50
languageName: node
linkType: hard
"volar-service-pug@npm:0.0.34":
version: 0.0.34
resolution: "volar-service-pug@npm:0.0.34"
dependencies:
"@volar/language-service": ~2.1.0
pug-lexer: ^5.0.1
pug-parser: ^6.0.0
volar-service-html: 0.0.34
vscode-html-languageservice: ^5.1.0
vscode-languageserver-textdocument: ^1.0.11
checksum: 4691aa1c8ea9039e1b5ce4218445309575c2cb4bc08ad5341a8af6f0db1a60711f26cc905e124c3485cc780eb58b895332fbb6a2ccf427a9d0e08012f2c5ad4a
languageName: node
linkType: hard
"vooks@npm:^0.2.12, vooks@npm:^0.2.4":
version: 0.2.12
resolution: "vooks@npm:0.2.12"
@ -7879,6 +7964,56 @@ browserlist@latest:
languageName: node
linkType: hard
"vscode-html-languageservice@npm:^5.1.0":
version: 5.1.2
resolution: "vscode-html-languageservice@npm:5.1.2"
dependencies:
"@vscode/l10n": ^0.0.18
vscode-languageserver-textdocument: ^1.0.11
vscode-languageserver-types: ^3.17.5
vscode-uri: ^3.0.8
checksum: 3a2a5ee5ad4ea429e85f4fb8f45da5b47d50541784d703fc9ccd009f68426034a48be6c04f8c420dc7236de07df93ccc28873da3395db5f5626fe169f18f1ac6
languageName: node
linkType: hard
"vscode-jsonrpc@npm:8.2.0":
version: 8.2.0
resolution: "vscode-jsonrpc@npm:8.2.0"
checksum: f302a01e59272adc1ae6494581fa31c15499f9278df76366e3b97b2236c7c53ebfc71efbace9041cfd2caa7f91675b9e56f2407871a1b3c7f760a2e2ee61484a
languageName: node
linkType: hard
"vscode-languageserver-protocol@npm:^3.17.5":
version: 3.17.5
resolution: "vscode-languageserver-protocol@npm:3.17.5"
dependencies:
vscode-jsonrpc: 8.2.0
vscode-languageserver-types: 3.17.5
checksum: dfb42d276df5dfea728267885b99872ecff62f6c20448b8539fae71bb196b420f5351c5aca7c1047bf8fb1f89fa94a961dce2bc5bf7e726198f4be0bb86a1e71
languageName: node
linkType: hard
"vscode-languageserver-textdocument@npm:^1.0.11":
version: 1.0.11
resolution: "vscode-languageserver-textdocument@npm:1.0.11"
checksum: ea7cdc9d4ffaae5952071fa11d17d714215a76444e6936c9359f94b9ba3222a52a55edb5bd5928bd3e9712b900a9f175bb3565ec1c8923234fe3bd327584bafb
languageName: node
linkType: hard
"vscode-languageserver-types@npm:3.17.5, vscode-languageserver-types@npm:^3.17.5":
version: 3.17.5
resolution: "vscode-languageserver-types@npm:3.17.5"
checksum: 79b420e7576398d396579ca3a461c9ed70e78db4403cd28bbdf4d3ed2b66a2b4114031172e51fad49f0baa60a2180132d7cb2ea35aa3157d7af3c325528210ac
languageName: node
linkType: hard
"vscode-uri@npm:^3.0.8":
version: 3.0.8
resolution: "vscode-uri@npm:3.0.8"
checksum: 514249126850c0a41a7d8c3c2836cab35983b9dc1938b903cfa253b9e33974c1416d62a00111385adcfa2b98df456437ab704f709a2ecca76a90134ef5eb4832
languageName: node
linkType: hard
"vue-demi@npm:>=0.14.5":
version: 0.14.5
resolution: "vue-demi@npm:0.14.5"