ci: merge main to release (#8503)

This commit is contained in:
Robert Sparks 2025-02-05 16:40:17 -06:00 committed by GitHub
commit 70bf2ae4dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1327 additions and 559 deletions

View file

@ -6,8 +6,8 @@
"": {
"name": "deploy-to-container",
"dependencies": {
"dockerode": "^4.0.3",
"fs-extra": "^11.2.0",
"dockerode": "^4.0.4",
"fs-extra": "^11.3.0",
"nanoid": "5.0.9",
"nanoid-dictionary": "5.0.0-beta.1",
"slugify": "1.6.6",
@ -258,6 +258,7 @@
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
@ -290,6 +291,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"dependencies": {
"tweetnacl": "^0.14.3"
}
@ -376,6 +378,7 @@
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
@ -389,9 +392,10 @@
}
},
"node_modules/docker-modem": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.5.tgz",
"integrity": "sha512-Cxw8uEcvNTRmsQuGqzzfiCnfGgf96tVJItLh8taOX0miTcIBALKH5TckCSuZbpbjP7uhAl81dOL9sxfa6HgCIg==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz",
"integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
@ -403,14 +407,15 @@
}
},
"node_modules/dockerode": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.3.tgz",
"integrity": "sha512-QSXJFcBQNaGZO6U3qWW4B7p8yRIJn/dWmvL2AQWfO/bjptBBO6QYdVkYSYFz9qoivP2jsOHZfmXMAfrK0BMKyg==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.4.tgz",
"integrity": "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==",
"license": "Apache-2.0",
"dependencies": {
"@balena/dockerignore": "^1.0.2",
"@grpc/grpc-js": "^1.11.1",
"@grpc/proto-loader": "^0.7.13",
"docker-modem": "^5.0.5",
"docker-modem": "^5.0.6",
"protobufjs": "^7.3.2",
"tar-fs": "~2.0.1",
"uuid": "^10.0.0"
@ -466,9 +471,10 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@ -651,12 +657,14 @@
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nan": {
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
"license": "MIT",
"optional": true
},
"node_modules/nanoid": {
@ -804,7 +812,8 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
@ -847,7 +856,8 @@
"node_modules/split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
"license": "ISC"
},
"node_modules/ssh2": {
"version": "1.16.0",
@ -1010,7 +1020,8 @@
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense"
},
"node_modules/undici-types": {
"version": "6.20.0",
@ -1411,9 +1422,9 @@
}
},
"docker-modem": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.5.tgz",
"integrity": "sha512-Cxw8uEcvNTRmsQuGqzzfiCnfGgf96tVJItLh8taOX0miTcIBALKH5TckCSuZbpbjP7uhAl81dOL9sxfa6HgCIg==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz",
"integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==",
"requires": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
@ -1422,14 +1433,14 @@
}
},
"dockerode": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.3.tgz",
"integrity": "sha512-QSXJFcBQNaGZO6U3qWW4B7p8yRIJn/dWmvL2AQWfO/bjptBBO6QYdVkYSYFz9qoivP2jsOHZfmXMAfrK0BMKyg==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.4.tgz",
"integrity": "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==",
"requires": {
"@balena/dockerignore": "^1.0.2",
"@grpc/grpc-js": "^1.11.1",
"@grpc/proto-loader": "^0.7.13",
"docker-modem": "^5.0.5",
"docker-modem": "^5.0.6",
"protobufjs": "^7.3.2",
"tar-fs": "~2.0.1",
"uuid": "^10.0.0"
@ -1473,9 +1484,9 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",

View file

@ -2,8 +2,8 @@
"name": "deploy-to-container",
"type": "module",
"dependencies": {
"dockerode": "^4.0.3",
"fs-extra": "^11.2.0",
"dockerode": "^4.0.4",
"fs-extra": "^11.3.0",
"nanoid": "5.0.9",
"nanoid-dictionary": "5.0.0-beta.1",
"slugify": "1.6.6",

View file

@ -6,11 +6,11 @@
"": {
"name": "diff",
"dependencies": {
"chalk": "^5.3.0",
"dockerode": "^4.0.2",
"chalk": "^5.4.1",
"dockerode": "^4.0.4",
"enquirer": "^2.4.1",
"extract-zip": "^2.0.1",
"fs-extra": "^11.2.0",
"fs-extra": "^11.3.0",
"got": "^13.0.0",
"keypress": "^0.2.1",
"listr2": "^6.6.1",
@ -29,6 +29,37 @@
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="
},
"node_modules/@grpc/grpc-js": {
"version": "1.12.5",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz",
"integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
},
"engines": {
"node": ">=12.10.0"
}
},
"node_modules/@grpc/proto-loader": {
"version": "0.7.13",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
"integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.2.5",
"yargs": "^17.7.2"
},
"bin": {
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -129,6 +160,16 @@
"node": ">=18.0.0"
}
},
"node_modules/@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -138,6 +179,70 @@
"node": ">=14"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@sindresorhus/is": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz",
@ -168,8 +273,7 @@
"node_modules/@types/node": {
"version": "18.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz",
"integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw==",
"optional": true
"integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw=="
},
"node_modules/@types/yauzl": {
"version": "2.10.0",
@ -228,6 +332,7 @@
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
@ -260,6 +365,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"dependencies": {
"tweetnacl": "^0.14.3"
}
@ -315,9 +421,10 @@
}
},
"node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
@ -440,14 +547,14 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
},
"node_modules/cpu-features": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz",
"integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==",
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"buildcheck": "~0.0.6",
"nan": "^2.17.0"
"nan": "^2.19.0"
},
"engines": {
"node": ">=10.0.0"
@ -516,9 +623,10 @@
}
},
"node_modules/docker-modem": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
"integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz",
"integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==",
"license": "Apache-2.0",
"dependencies": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
@ -530,13 +638,18 @@
}
},
"node_modules/dockerode": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz",
"integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.4.tgz",
"integrity": "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==",
"license": "Apache-2.0",
"dependencies": {
"@balena/dockerignore": "^1.0.2",
"docker-modem": "^5.0.3",
"tar-fs": "~2.0.1"
"@grpc/grpc-js": "^1.11.1",
"@grpc/proto-loader": "^0.7.13",
"docker-modem": "^5.0.6",
"protobufjs": "^7.3.2",
"tar-fs": "~2.0.1",
"uuid": "^10.0.0"
},
"engines": {
"node": ">= 8.0"
@ -666,9 +779,10 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@ -949,6 +1063,12 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"license": "MIT"
},
"node_modules/log-update": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz",
@ -1040,6 +1160,12 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/long": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
"integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==",
"license": "Apache-2.0"
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
@ -1145,9 +1271,10 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nan": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
"integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
"license": "MIT",
"optional": true
},
"node_modules/normalize-url": {
@ -1230,6 +1357,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/protobufjs": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -1349,7 +1500,8 @@
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
@ -1415,12 +1567,13 @@
"node_modules/split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
"license": "ISC"
},
"node_modules/ssh2": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz",
"integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.6",
@ -1430,8 +1583,8 @@
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.9",
"nan": "^2.18.0"
"cpu-features": "~0.0.10",
"nan": "^2.20.0"
}
},
"node_modules/string_decoder": {
@ -1578,7 +1731,8 @@
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense"
},
"node_modules/type-fest": {
"version": "1.4.0",
@ -1604,6 +1758,19 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -1713,6 +1880,26 @@
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="
},
"@grpc/grpc-js": {
"version": "1.12.5",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz",
"integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==",
"requires": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
}
},
"@grpc/proto-loader": {
"version": "0.7.13",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
"integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
"requires": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
"protobufjs": "^7.2.5",
"yargs": "^17.7.2"
}
},
"@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1779,12 +1966,71 @@
"minipass": "^7.0.4"
}
},
"@js-sdsl/ordered-map": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="
},
"@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true
},
"@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
},
"@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
},
"@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
},
"@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"requires": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
},
"@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
},
"@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
},
"@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"@sindresorhus/is": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz",
@ -1806,8 +2052,7 @@
"@types/node": {
"version": "18.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.5.tgz",
"integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw==",
"optional": true
"integrity": "sha512-Xjt5ZGUa5WusGZJ4WJPbOT8QOqp6nDynVFRKcUt32bOgvXEoc6o085WNkYTMO7ifAj2isEfQQ2cseE+wT6jsRw=="
},
"@types/yauzl": {
"version": "2.10.0",
@ -1909,9 +2154,9 @@
}
},
"chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="
},
"chownr": {
"version": "1.1.4",
@ -1994,13 +2239,13 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
},
"cpu-features": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz",
"integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==",
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
"optional": true,
"requires": {
"buildcheck": "~0.0.6",
"nan": "^2.17.0"
"nan": "^2.19.0"
}
},
"cross-spawn": {
@ -2042,9 +2287,9 @@
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
},
"docker-modem": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.3.tgz",
"integrity": "sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg==",
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz",
"integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==",
"requires": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
@ -2053,13 +2298,17 @@
}
},
"dockerode": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.2.tgz",
"integrity": "sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.4.tgz",
"integrity": "sha512-6GYP/EdzEY50HaOxTVTJ2p+mB5xDHTMJhS+UoGrVyS6VC+iQRh7kZ4FRpUYq6nziby7hPqWhOrFFUFTMUZJJ5w==",
"requires": {
"@balena/dockerignore": "^1.0.2",
"docker-modem": "^5.0.3",
"tar-fs": "~2.0.1"
"@grpc/grpc-js": "^1.11.1",
"@grpc/proto-loader": "^0.7.13",
"docker-modem": "^5.0.6",
"protobufjs": "^7.3.2",
"tar-fs": "~2.0.1",
"uuid": "^10.0.0"
}
},
"eastasianwidth": {
@ -2155,9 +2404,9 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@ -2342,6 +2591,11 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
},
"log-update": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz",
@ -2399,6 +2653,11 @@
}
}
},
"long": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz",
"integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg=="
},
"lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
@ -2462,9 +2721,9 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nan": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
"integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
"optional": true
},
"normalize-url": {
@ -2517,6 +2776,25 @@
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz",
"integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ=="
},
"protobufjs": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/node": ">=13.7.0",
"long": "^5.0.0"
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -2636,14 +2914,14 @@
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="
},
"ssh2": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz",
"integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
"requires": {
"asn1": "^0.2.6",
"bcrypt-pbkdf": "^1.0.2",
"cpu-features": "~0.0.9",
"nan": "^2.18.0"
"cpu-features": "~0.0.10",
"nan": "^2.20.0"
}
},
"string_decoder": {
@ -2774,6 +3052,11 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -2,11 +2,11 @@
"name": "diff",
"type": "module",
"dependencies": {
"chalk": "^5.3.0",
"dockerode": "^4.0.2",
"chalk": "^5.4.1",
"dockerode": "^4.0.4",
"enquirer": "^2.4.1",
"extract-zip": "^2.0.1",
"fs-extra": "^11.2.0",
"fs-extra": "^11.3.0",
"got": "^13.0.0",
"keypress": "^0.2.1",
"listr2": "^6.6.1",

View file

@ -101,6 +101,7 @@ services:
# stop_grace_period: 1m
# volumes:
# - .:/workspace
# - app-assets:/assets
volumes:
postgresdb-data:

View file

@ -7,7 +7,7 @@ import factory
import factory.fuzzy
import datetime
from typing import Optional # pyflakes:ignore
from typing import Any # pyflakes:ignore
from django.conf import settings
from django.utils import timezone
@ -37,13 +37,16 @@ class BaseDocumentFactory(factory.django.DjangoModelFactory):
model = Document
skip_postgeneration_save = True
# n.b., a few attributes are typed as Any so mypy won't complain when we override in subclasses
title = factory.Faker('sentence',nb_words=5)
abstract = factory.Faker('paragraph', nb_sentences=5)
abstract: Any = factory.Faker('paragraph', nb_sentences=5)
rev = '00'
std_level_id = None # type: Optional[str]
std_level_id: Any = None
intended_std_level_id = None
time = timezone.now()
expires = factory.LazyAttribute(lambda o: o.time+datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE))
expires: Any = factory.LazyAttribute(
lambda o: o.time+datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE)
)
pages = factory.fuzzy.FuzzyInteger(2,400)
@ -282,7 +285,7 @@ class DocEventFactory(factory.django.DjangoModelFactory):
type = 'added_comment'
by = factory.SubFactory('ietf.person.factories.PersonFactory')
doc = factory.SubFactory(DocumentFactory)
doc: Any = factory.SubFactory(DocumentFactory) # `Any` to appease mypy when a subclass overrides doc
desc = factory.Faker('sentence',nb_words=6)
@factory.lazy_attribute

View file

@ -1,17 +0,0 @@
# Copyright The IETF Trust 2014-2021, All Rights Reserved
# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand, CommandError
from ietf.ipr.utils import generate_draft_recursive_txt
class Command(BaseCommand):
help = ("Generate machine-readable list of IPR disclosures by Internet-Draft name (recursive)")
def handle(self, *args, **options):
try:
generate_draft_recursive_txt()
except (ValueError, IOError) as e:
raise CommandError(e)

View file

@ -161,12 +161,6 @@ class IprTests(TestCase):
r = self.client.get(urlreverse("ietf.ipr.views.history", kwargs=dict(id=ipr.pk)))
self.assertContains(r, ipr.title)
def test_iprs_for_drafts(self):
draft=WgDraftFactory()
ipr = HolderIprDisclosureFactory(docs=[draft,])
r = self.client.get(urlreverse("ietf.ipr.views.by_draft_txt"))
self.assertContains(r, draft.name)
self.assertContains(r, str(ipr.pk))
def test_about(self):
r = self.client.get(urlreverse("ietf.ipr.views.about"))
@ -732,7 +726,7 @@ I would like to revoke this declaration.
self.assertIn(f'{settings.IDTRACKER_BASE_URL}{urlreverse("ietf.ipr.views.showlist")}', get_payload_text(outbox[1]).replace('\n',' '))
def send_ipr_email_helper(self) -> tuple[str, IprEvent, HolderIprDisclosure]:
ipr = HolderIprDisclosureFactory()
ipr = HolderIprDisclosureFactory.create() # call create() explicitly so mypy sees correct type
url = urlreverse('ietf.ipr.views.email',kwargs={ "id": ipr.id })
self.client.login(username="secretary", password="secretary+password")
yesterday = date_today() - datetime.timedelta(1)

View file

@ -12,8 +12,6 @@ urlpatterns = [
url(r'^admin/$', RedirectView.as_view(url=reverse_lazy('ietf.ipr.views.admin',kwargs={'state':'pending'}), permanent=True)),
url(r'^admin/(?P<state>pending|removed|parked)/$', views.admin),
url(r'^ajax/search/$', views.ajax_search),
url(r'^by-draft/$', views.by_draft_txt),
url(r'^by-draft-recursive/$', views.by_draft_recursive_txt),
url(r'^(?P<id>\d+)/$', views.show),
url(r'^(?P<id>\d+)/addcomment/$', views.add_comment),
url(r'^(?P<id>\d+)/addemail/$', views.add_email),

View file

@ -4,7 +4,6 @@
from textwrap import dedent
from ietf.ipr.mail import process_response_email, UndeliverableIprResponseError
from ietf.ipr.models import IprDocRel
import debug # pyflakes:ignore
@ -64,30 +63,6 @@ def related_docs(doc, relationship=('replaces', 'obs'), reverse_relationship=("b
return list(set(results))
def generate_draft_recursive_txt():
docipr = {}
for o in IprDocRel.objects.filter(disclosure__state='posted').select_related('document'):
doc = o.document
name = doc.name
related_set = set(doc) | set(doc.all_related_that_doc(('obs', 'replaces')))
for related in related_set:
name = related.name
if name.startswith("rfc"):
name = name.upper()
if not name in docipr:
docipr[name] = []
docipr[name].append(o.disclosure_id)
lines = [ "# Machine-readable list of IPR disclosures by Internet-Draft name" ]
for name, iprs in docipr.items():
lines.append(name + "\t" + "\t".join(str(ipr_id) for ipr_id in sorted(iprs)))
data = '\n'.join(lines)
filename = '/a/ietfdata/derived/ipr_draft_recursive.txt'
with open(filename, 'w') as f:
f.write(data)
def ingest_response_email(message: bytes):
from ietf.api.views import EmailIngestionError # avoid circular import

View file

@ -445,35 +445,6 @@ def history(request, id):
'selected_tab_entry':'history'
})
def by_draft_txt(request):
docipr = {}
for o in IprDocRel.objects.filter(disclosure__state='posted').select_related('document'):
name = o.document.name
if name.startswith("rfc"):
name = name.upper()
if not name in docipr:
docipr[name] = []
docipr[name].append(o.disclosure_id)
lines = [ "# Machine-readable list of IPR disclosures by draft name" ]
for name, iprs in docipr.items():
lines.append(name + "\t" + "\t".join(str(ipr_id) for ipr_id in sorted(iprs)))
return HttpResponse("\n".join(lines), content_type="text/plain; charset=%s"%settings.DEFAULT_CHARSET)
def by_draft_recursive_txt(request):
"""Returns machine-readable list of IPR disclosures by draft name, recursive.
NOTE: this view is expensive and should be removed _after_ tools.ietf.org is retired,
including util function and management commands that generate the content for
this view."""
with open('/a/ietfdata/derived/ipr_draft_recursive.txt') as f:
content = f.read()
return HttpResponse(content, content_type="text/plain; charset=%s"%settings.DEFAULT_CHARSET)
def new(request, _type, updates=None):
"""Submit a new IPR Disclosure. If the updates field != None, this disclosure

View file

@ -3,10 +3,42 @@
# Celery task definitions
#
from celery import shared_task
from django.utils import timezone
from ietf.utils import log
from .models import Meeting
from .utils import generate_proceedings_content
from .views import generate_agenda_data
@shared_task
def agenda_data_refresh():
generate_agenda_data(force_refresh=True)
@shared_task
def proceedings_content_refresh_task(*, all=False):
"""Refresh meeting proceedings cache
If `all` is `False`, then refreshes the cache for meetings whose numbers modulo
24 equal the current hour number (0-23). Scheduling the task once per hour will
then result in all proceedings being recomputed daily, with no more than two per
hour (now) or a few per hour in the next decade. That keeps the computation time
to under a couple minutes on our current production system.
If `all` is True, refreshes all meetings
"""
now = timezone.now()
for meeting in Meeting.objects.filter(type_id="ietf").order_by("number"):
if meeting.proceedings_format_version == 1:
continue # skip v1 proceedings, they're stored externally
num = meeting.get_number() # convert str -> int
if num is None:
log.log(
f"Not refreshing proceedings for meeting {meeting.number}: "
f"type is 'ietf' but get_number() returned None"
)
elif all or (num % 24 == now.hour):
log.log(f"Refreshing proceedings for meeting {meeting.number}...")
generate_proceedings_content(meeting, force_refresh=True)

View file

@ -0,0 +1,51 @@
# Copyright The IETF Trust 2025, All Rights Reserved
import datetime
from mock import patch, call
from ietf.utils.test_utils import TestCase
from .factories import MeetingFactory
from .tasks import proceedings_content_refresh_task, agenda_data_refresh
class TaskTests(TestCase):
@patch("ietf.meeting.tasks.generate_agenda_data")
def test_agenda_data_refresh(self, mock_generate):
agenda_data_refresh()
self.assertTrue(mock_generate.called)
self.assertEqual(mock_generate.call_args, call(force_refresh=True))
@patch("ietf.meeting.tasks.generate_proceedings_content")
def test_proceedings_content_refresh_task(self, mock_generate):
# Generate a couple of meetings
meeting120 = MeetingFactory(type_id="ietf", number="120") # 24 * 5
meeting127 = MeetingFactory(type_id="ietf", number="127") # 24 * 5 + 7
# Times to be returned
now_utc = datetime.datetime.now(tz=datetime.timezone.utc)
hour_00_utc = now_utc.replace(hour=0)
hour_01_utc = now_utc.replace(hour=1)
hour_07_utc = now_utc.replace(hour=7)
# hour 00 - should call meeting with number % 24 == 0
with patch("ietf.meeting.tasks.timezone.now", return_value=hour_00_utc):
proceedings_content_refresh_task()
self.assertEqual(mock_generate.call_count, 1)
self.assertEqual(mock_generate.call_args, call(meeting120, force_refresh=True))
mock_generate.reset_mock()
# hour 01 - should call no meetings
with patch("ietf.meeting.tasks.timezone.now", return_value=hour_01_utc):
proceedings_content_refresh_task()
self.assertEqual(mock_generate.call_count, 0)
# hour 07 - should call meeting with number % 24 == 0
with patch("ietf.meeting.tasks.timezone.now", return_value=hour_07_utc):
proceedings_content_refresh_task()
self.assertEqual(mock_generate.call_count, 1)
self.assertEqual(mock_generate.call_args, call(meeting127, force_refresh=True))
mock_generate.reset_mock()
# With all=True, all should be called regardless of time. Reuse hour_01_utc which called none before
with patch("ietf.meeting.tasks.timezone.now", return_value=hour_01_utc):
proceedings_content_refresh_task(all=True)
self.assertEqual(mock_generate.call_count, 2)

View file

@ -32,6 +32,7 @@ from django.db.models import F, Max
from django.http import QueryDict, FileResponse
from django.template import Context, Template
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.text import slugify
import debug # pyflakes:ignore
@ -46,9 +47,9 @@ from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_
from ietf.meeting.helpers import send_interim_minutes_reminder, populate_important_dates, update_important_dates
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent, Room, Constraint, ConstraintName
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data
from ietf.meeting.utils import condition_slide_order
from ietf.meeting.utils import condition_slide_order, generate_proceedings_content
from ietf.meeting.utils import add_event_info_to_session_qs, participants_for_meeting
from ietf.meeting.utils import create_recording, get_next_sequence, bluesheet_data
from ietf.meeting.utils import create_recording, delete_recording, get_next_sequence, bluesheet_data
from ietf.meeting.views import session_draft_list, parse_agenda_filter_params, sessions_post_save, agenda_extract_schedule
from ietf.meeting.views import get_summary_by_area, get_summary_by_type, get_summary_by_purpose, generate_agenda_data
from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, ProceedingsMaterialTypeName
@ -441,6 +442,48 @@ class MeetingTests(BaseMeetingTestCase):
self.assertIn(new_recording_title, links[0].text_content())
#debug.show("q(f'#notes_and_recordings_{session_pk}')")
def test_delete_recordings(self):
# No user specified, active recording state
sp = SessionPresentationFactory(
document__type_id="recording",
document__external_url="https://example.com/some-recording",
document__states=[("recording", "active")],
)
doc = sp.document
doc.docevent_set.all().delete() # clear this out
delete_recording(sp)
self.assertFalse(SessionPresentation.objects.filter(pk=sp.pk).exists())
self.assertEqual(doc.get_state("recording").slug, "deleted", "recording state updated")
self.assertEqual(doc.docevent_set.count(), 1, "one event added")
event = doc.docevent_set.first()
self.assertEqual(event.type, "changed_state", "event is a changed_state event")
self.assertEqual(event.by.name, "(System)", "system user is responsible")
# Specified user, no recording state
sp = SessionPresentationFactory(
document__type_id="recording",
document__external_url="https://example.com/some-recording",
document__states=[],
)
doc = sp.document
doc.docevent_set.all().delete() # clear this out
user = PersonFactory() # naming matches the methods - user is a Person, not a User
delete_recording(sp, user=user)
self.assertFalse(SessionPresentation.objects.filter(pk=sp.pk).exists())
self.assertEqual(doc.get_state("recording").slug, "deleted", "recording state updated")
self.assertEqual(doc.docevent_set.count(), 1, "one event added")
event = doc.docevent_set.first()
self.assertEqual(event.type, "changed_state", "event is a changed_state event")
self.assertEqual(event.by, user, "user is responsible")
# Document is not a recording
sp = SessionPresentationFactory(
document__type_id="draft",
document__external_url="https://example.com/some-recording",
)
with self.assertRaises(ValueError):
delete_recording(sp)
def test_agenda_ical_next_meeting_type(self):
# start with no upcoming IETF meetings, just an interim
MeetingFactory(
@ -2082,7 +2125,8 @@ class EditTimeslotsTests(TestCase):
@staticmethod
def create_bare_meeting(number=120) -> Meeting:
"""Create a basic IETF meeting"""
return MeetingFactory(
# Call create() explicitly so mypy sees the correct type
return MeetingFactory.create(
type_id='ietf',
number=number,
date=date_today() + datetime.timedelta(days=10),
@ -7363,6 +7407,118 @@ class SessionTests(TestCase):
self.assertEqual(r.status_code,302)
self.assertEqual(len(outbox),1)
@override_settings(YOUTUBE_DOMAINS=["youtube.com"])
def test_add_session_recordings(self):
session = SessionFactory(meeting__type_id="ietf")
url = urlreverse(
"ietf.meeting.views.add_session_recordings",
kwargs={"session_id": session.pk, "num": session.meeting.number},
)
# does not fully validate authorization for non-secretariat users :-(
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
pq = PyQuery(r.content)
title_input = pq("input#id_title")
self.assertIsNotNone(title_input)
self.assertEqual(
title_input.attr.value,
"Video recording of {acro} for {timestamp}".format(
acro=session.group.acronym,
timestamp=session.official_timeslotassignment().timeslot.utc_start_time().strftime(
"%Y-%m-%d %H:%M"
),
),
)
with patch("ietf.meeting.views.create_recording") as mock_create:
r = self.client.post(
url,
data={
"title": "This is my video title",
"url": "",
}
)
self.assertFalse(mock_create.called)
with patch("ietf.meeting.views.create_recording") as mock_create:
r = self.client.post(
url,
data={
"title": "This is my video title",
"url": "https://yubtub.com/this-is-not-a-youtube-video",
}
)
self.assertFalse(mock_create.called)
with patch("ietf.meeting.views.create_recording") as mock_create:
r = self.client.post(
url,
data={
"title": "This is my video title",
"url": "https://youtube.com/finally-a-video",
}
)
self.assertTrue(mock_create.called)
self.assertEqual(
mock_create.call_args,
call(
session,
"https://youtube.com/finally-a-video",
title="This is my video title",
user=Person.objects.get(user__username="secretary"),
),
)
# CAN delete session presentation for this session
sp = SessionPresentationFactory(
session=session,
document__type_id="recording",
document__external_url="https://example.com/some-video",
)
with patch("ietf.meeting.views.delete_recording") as mock_delete:
r = self.client.post(
url,
data={
"delete": str(sp.pk),
}
)
self.assertEqual(r.status_code, 200)
self.assertTrue(mock_delete.called)
self.assertEqual(mock_delete.call_args, call(sp))
# ValueError message from delete_recording does not reach the user
sp = SessionPresentationFactory(
session=session,
document__type_id="recording",
document__external_url="https://example.com/some-video",
)
with patch("ietf.meeting.views.delete_recording", side_effect=ValueError("oh joy!")) as mock_delete:
r = self.client.post(
url,
data={
"delete": str(sp.pk),
}
)
self.assertTrue(mock_delete.called)
self.assertNotContains(r, "oh joy!", status_code=200)
# CANNOT delete session presentation for a different session
sp_for_other_session = SessionPresentationFactory(
document__type_id="recording",
document__external_url="https://example.com/some-other-video",
)
with patch("ietf.meeting.views.delete_recording") as mock_delete:
r = self.client.post(
url,
data={
"delete": str(sp_for_other_session.pk),
}
)
self.assertEqual(r.status_code, 404)
self.assertFalse(mock_delete.called)
class HasMeetingsTests(TestCase):
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
@ -8141,8 +8297,7 @@ class ProceedingsTests(BaseMeetingTestCase):
path = Path(settings.BASE_DIR) / 'meeting/test_procmat.pdf'
return path.open('rb')
def _assertMeetingHostsDisplayed(self, response, meeting):
pq = PyQuery(response.content)
def _assertMeetingHostsDisplayed(self, pq: PyQuery, meeting):
host_divs = pq('div.host-logo')
self.assertEqual(len(host_divs), meeting.meetinghosts.count(), 'Should have a logo for every meeting host')
self.assertEqual(
@ -8158,12 +8313,11 @@ class ProceedingsTests(BaseMeetingTestCase):
'Correct image and name for each host should appear in the correct order'
)
def _assertProceedingsMaterialsDisplayed(self, response, meeting):
def _assertProceedingsMaterialsDisplayed(self, pq: PyQuery, meeting):
"""Checks that all (and only) active materials are linked with correct href and title"""
expected_materials = [
m for m in meeting.proceedings_materials.order_by('type__order') if m.active()
]
pq = PyQuery(response.content)
links = pq('div.proceedings-material a')
self.assertEqual(len(links), len(expected_materials), 'Should have an entry for each active ProceedingsMaterial')
self.assertEqual(
@ -8172,9 +8326,8 @@ class ProceedingsTests(BaseMeetingTestCase):
'Correct title and link for each ProceedingsMaterial should appear in the correct order'
)
def _assertGroupSessions(self, response, meeting):
def _assertGroupSessions(self, pq: PyQuery):
"""Checks that group/sessions are present"""
pq = PyQuery(response.content)
sections = ["plenaries", "gen", "iab", "editorial", "irtf", "training"]
for section in sections:
self.assertEqual(len(pq(f"#{section}")), 1, f"{section} section should exists in proceedings")
@ -8182,10 +8335,9 @@ class ProceedingsTests(BaseMeetingTestCase):
def test_proceedings(self):
"""Proceedings should be displayed correctly
Currently only tests that the view responds with a 200 response code and checks the ProceedingsMaterials
at the top of the proceedings. Ought to actually test the display of the individual group/session
materials as well.
Proceedings contents are tested in detail when testing generate_proceedings_content.
"""
# number must be >97 (settings.PROCEEDINGS_VERSION_CHANGES)
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
GroupEventFactory(group=session.group,type='status_update')
@ -8210,16 +8362,72 @@ class ProceedingsTests(BaseMeetingTestCase):
self._create_proceedings_materials(meeting)
url = urlreverse("ietf.meeting.views.proceedings", kwargs=dict(num=meeting.number))
r = self.client.get(url)
cached_content = mark_safe("<p>Fake proceedings content</p>")
with patch("ietf.meeting.views.generate_proceedings_content") as mock_gpc:
mock_gpc.return_value = cached_content
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertIn(cached_content, r.content.decode())
self.assertTemplateUsed(r, "meeting/proceedings_wrapper.html")
self.assertTemplateNotUsed(r, "meeting/proceedings.html")
# These are rendered in proceedings_wrapper.html, so test them here
if len(meeting.city) > 0:
self.assertContains(r, meeting.city)
if len(meeting.venue_name) > 0:
self.assertContains(r, meeting.venue_name)
self._assertMeetingHostsDisplayed(PyQuery(r.content), meeting)
@patch("ietf.meeting.utils.caches")
def test_generate_proceedings_content(self, mock_caches):
# number must be >97 (settings.PROCEEDINGS_VERSION_CHANGES)
meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100'))
# First, check that by default a value in the cache is used without doing any other computation
mock_default_cache = mock_caches["default"]
mock_default_cache.get.return_value = "a cached value"
result = generate_proceedings_content(meeting)
self.assertEqual(result, "a cached value")
self.assertFalse(mock_default_cache.set.called)
self.assertTrue(mock_default_cache.get.called)
cache_key = mock_default_cache.get.call_args.args[0]
mock_default_cache.get.reset_mock()
# Now set up for actual computation of the proceedings content.
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
GroupEventFactory(group=session.group,type='status_update')
SessionPresentationFactory(document__type_id='recording',session=session)
SessionPresentationFactory(document__type_id='recording',session=session,document__title="Audio recording for tests")
# Add various group sessions
groups = []
parent_groups = [
GroupFactory.create(type_id="area", acronym="gen"),
GroupFactory.create(acronym="iab"),
GroupFactory.create(acronym="irtf"),
]
for parent in parent_groups:
groups.append(GroupFactory.create(parent=parent))
for acronym in ["rsab", "edu"]:
groups.append(GroupFactory.create(acronym=acronym))
for group in groups:
SessionFactory(meeting=meeting, group=group)
self.write_materials_files(meeting, session)
self._create_proceedings_materials(meeting)
# Now "empty" the mock cache and see that we compute the expected proceedings content.
mock_default_cache.get.return_value = None
proceedings_content = generate_proceedings_content(meeting)
self.assertTrue(mock_default_cache.get.called)
self.assertEqual(mock_default_cache.get.call_args.args[0], cache_key, "same cache key each time")
self.assertTrue(mock_default_cache.set.called)
self.assertEqual(mock_default_cache.set.call_args, call(cache_key, proceedings_content, timeout=86400))
mock_default_cache.get.reset_mock()
mock_default_cache.set.reset_mock()
# standard items on every proceedings
pq = PyQuery(r.content)
pq = PyQuery(proceedings_content)
self.assertNotEqual(
pq('a[href="{}"]'.format(
urlreverse('ietf.meeting.views.proceedings_overview', kwargs=dict(num=meeting.number)))
@ -8250,9 +8458,17 @@ class ProceedingsTests(BaseMeetingTestCase):
)
# configurable contents
self._assertMeetingHostsDisplayed(r, meeting)
self._assertProceedingsMaterialsDisplayed(r, meeting)
self._assertGroupSessions(r, meeting)
self._assertProceedingsMaterialsDisplayed(pq, meeting)
self._assertGroupSessions(pq)
# Finally, repeat the first cache test, but now with force_refresh=True. The cached value
# should be ignored and we should recompute the proceedings as before.
mock_default_cache.get.return_value = "a cached value"
result = generate_proceedings_content(meeting, force_refresh=True)
self.assertEqual(result, proceedings_content) # should have recomputed the same thing
self.assertFalse(mock_default_cache.get.called, "don't bother reading cache when force_refresh is True")
self.assertTrue(mock_default_cache.set.called)
self.assertEqual(mock_default_cache.set.call_args, call(cache_key, proceedings_content, timeout=86400))
def test_named_session(self):
"""Session with a name should appear separately in the proceedings"""

View file

@ -16,6 +16,7 @@ class AgendaRedirectView(RedirectView):
safe_for_all_meeting_types = [
url(r'^session/(?P<acronym>[-a-z0-9]+)/?$', views.session_details),
url(r'^session/(?P<session_id>\d+)/drafts$', views.add_session_drafts),
url(r'^session/(?P<session_id>\d+)/recordings$', views.add_session_recordings),
url(r'^session/(?P<session_id>\d+)/attendance$', views.session_attendance),
url(r'^session/(?P<session_id>\d+)/bluesheets$', views.upload_session_bluesheets),
url(r'^session/(?P<session_id>\d+)/minutes$', views.upload_session_minutes),
@ -63,7 +64,8 @@ type_ietf_only_patterns = [
type_interim_patterns = [
url(r'^agenda/(?P<acronym>[A-Za-z0-9-]+)-drafts.pdf$', views.session_draft_pdf),
url(r'^agenda/(?P<acronym>[A-Za-z0-9-]+)-drafts.tgz$', views.session_draft_tarfile),
url(r'^materials/%(document)s((?P<ext>\.[a-z0-9]+)|/)?$' % settings.URL_REGEXPS, views.materials_document),
url(r'^materials/%(document)s(?P<ext>\.[a-z0-9]+)$' % settings.URL_REGEXPS, views.materials_document),
url(r'^materials/%(document)s/?$' % settings.URL_REGEXPS, views.materials_document),
url(r'^agenda.json$', views.agenda_json)
]

View file

@ -3,16 +3,19 @@
import datetime
import itertools
import os
from hashlib import sha384
import pytz
import subprocess
import tempfile
from collections import defaultdict
from pathlib import Path
from django.conf import settings
from django.contrib import messages
from django.db.models import OuterRef, Subquery, TextField, Q, Value
from django.core.cache import caches
from django.core.files.base import ContentFile
from django.db.models import OuterRef, Subquery, TextField, Q, Value, Max
from django.db.models.functions import Coalesce
from django.template.loader import render_to_string
from django.utils import timezone
@ -23,7 +26,7 @@ import debug # pyflakes:ignore
from ietf.dbtemplate.models import DBTemplate
from ietf.meeting.models import (Session, SchedulingEvent, TimeSlot,
Constraint, SchedTimeSessAssignment, SessionPresentation, Attended)
from ietf.doc.models import Document, State, NewRevisionDocEvent
from ietf.doc.models import Document, State, NewRevisionDocEvent, StateDocEvent
from ietf.doc.models import DocEvent
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
@ -225,12 +228,7 @@ def generate_bluesheet(request, session):
'session': session,
'data': data,
})
fd, name = tempfile.mkstemp(suffix=".txt", text=True)
os.close(fd)
with open(name, "w") as file:
file.write(text)
with open(name, "br") as file:
return save_bluesheet(request, session, file)
return save_bluesheet(request, session, ContentFile(text.encode("utf-8"), name="unusednamepartsothereisanextension.txt"))
def finalize(request, meeting):
@ -853,6 +851,26 @@ def create_recording(session, url, title=None, user=None):
return doc
def delete_recording(session_presentation, user=None):
"""Delete a session recording"""
document = session_presentation.document
if document.type_id != "recording":
raise ValueError(f"Document {document.pk} is not a recording (type_id={document.type_id})")
recording_state = document.get_state("recording")
deleted_state = State.objects.get(type_id="recording", slug="deleted")
if recording_state != deleted_state:
# Update the recording state and create a history event
document.set_state(deleted_state)
StateDocEvent.objects.create(
type="changed_state",
by=user or Person.objects.get(name="(System)"),
doc=document,
rev=document.rev,
state_type=deleted_state.type,
state=deleted_state,
)
session_presentation.delete()
def get_next_sequence(group, meeting, type):
'''
Returns the next sequence number to use for a document of type = type.
@ -980,3 +998,169 @@ def participants_for_meeting(meeting):
sessions = meeting.session_set.filter(Q(type='plenary') | Q(group__type__in=['wg', 'rg']))
attended = Attended.objects.filter(session__in=sessions).values_list('person', flat=True).distinct()
return (checked_in, attended)
def generate_proceedings_content(meeting, force_refresh=False):
"""Render proceedings content for a meeting and update cache
:meeting: meeting whose proceedings should be rendered
:force_refresh: true to force regeneration and cache refresh
"""
cache = caches["default"]
cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"]
# Include proceedings_final in the bare_key so we'll always reflect that accurately, even at the cost of
# a recomputation in the view
bare_key = f"proceedings.{meeting.number}.{cache_version}.final={meeting.proceedings_final}"
cache_key = sha384(bare_key.encode("utf8")).hexdigest()
if not force_refresh:
cached_content = cache.get(cache_key, None)
if cached_content is not None:
return cached_content
def area_and_group_acronyms_from_session(s):
area = s.group_parent_at_the_time()
if area == None:
area = s.group.parent
group = s.group_at_the_time()
return (area.acronym, group.acronym)
schedule = meeting.schedule
sessions = (
meeting.session_set.with_current_status()
.filter(Q(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None])
| Q(current_status='notmeet'))
.select_related()
.order_by('-current_status')
)
plenaries, _ = organize_proceedings_sessions(
sessions.filter(name__icontains='plenary')
.exclude(current_status='notmeet')
)
irtf_meeting, irtf_not_meeting = organize_proceedings_sessions(
sessions.filter(group__parent__acronym = 'irtf').order_by('group__acronym')
)
# per Colin (datatracker #5010) - don't report not meeting rags
irtf_not_meeting = [item for item in irtf_not_meeting if item["group"].type_id != "rag"]
irtf = {"meeting_groups":irtf_meeting, "not_meeting_groups":irtf_not_meeting}
training, _ = organize_proceedings_sessions(
sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other',])
.exclude(current_status='notmeet')
)
iab, _ = organize_proceedings_sessions(
sessions.filter(group__parent__acronym = 'iab')
.exclude(current_status='notmeet')
)
editorial, _ = organize_proceedings_sessions(
sessions.filter(group__acronym__in=['rsab','rswg'])
.exclude(current_status='notmeet')
)
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym__in=['edu','iepg','tools'])
ietf = list(ietf)
ietf.sort(key=lambda s: area_and_group_acronyms_from_session(s))
ietf_areas = []
for area, area_sessions in itertools.groupby(ietf, key=lambda s: s.group_parent_at_the_time()):
meeting_groups, not_meeting_groups = organize_proceedings_sessions(area_sessions)
ietf_areas.append((area, meeting_groups, not_meeting_groups))
with timezone.override(meeting.tz()):
rendered_content = render_to_string(
"meeting/proceedings.html",
{
'meeting': meeting,
'plenaries': plenaries,
'training': training,
'irtf': irtf,
'iab': iab,
'editorial': editorial,
'ietf_areas': ietf_areas,
'meetinghost_logo': {
'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT,
'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH,
}
},
)
cache.set(
cache_key,
rendered_content,
timeout=86400, # one day, in seconds
)
return rendered_content
def organize_proceedings_sessions(sessions):
# Collect sessions by Group, then bin by session name (including sessions with blank names).
# If all of a group's sessions are 'notmeet', the processed data goes in not_meeting_sessions.
# Otherwise, the data goes in meeting_sessions.
meeting_groups = []
not_meeting_groups = []
for group_acronym, group_sessions in itertools.groupby(sessions, key=lambda s: s.group.acronym):
by_name = {}
is_meeting = False
all_canceled = True
group = None
for s in sorted(
group_sessions,
key=lambda gs: (
gs.official_timeslotassignment().timeslot.time
if gs.official_timeslotassignment() else datetime.datetime(datetime.MAXYEAR, 1, 1)
),
):
group = s.group
if s.current_status != 'notmeet':
is_meeting = True
if s.current_status != 'canceled':
all_canceled = False
by_name.setdefault(s.name, [])
if s.current_status != 'notmeet' or s.presentations.exists():
by_name[s.name].append(s) # for notmeet, only include sessions with materials
for sess_name, ss in by_name.items():
session = ss[0] if ss else None
def _format_materials(items):
"""Format session/material for template
Input is a list of (session, materials) pairs. The materials value can be a single value or a list.
"""
material_times = {} # key is material, value is first timestamp it appeared
for s, mats in items:
tsa = s.official_timeslotassignment()
timestamp = tsa.timeslot.time if tsa else None
if not isinstance(mats, list):
mats = [mats]
for mat in mats:
if mat and mat not in material_times:
material_times[mat] = timestamp
n_mats = len(material_times)
result = []
if n_mats == 1:
result.append({'material': list(material_times)[0]}) # no 'time' when only a single material
elif n_mats > 1:
for mat, timestamp in material_times.items():
result.append({'material': mat, 'time': timestamp})
return result
entry = {
'group': group,
'name': sess_name,
'session': session,
'canceled': all_canceled,
'has_materials': s.presentations.exists(),
'agendas': _format_materials((s, s.agenda()) for s in ss),
'minutes': _format_materials((s, s.minutes()) for s in ss),
'bluesheets': _format_materials((s, s.bluesheets()) for s in ss),
'recordings': _format_materials((s, s.recordings()) for s in ss),
'meetecho_recordings': _format_materials((s, [s.session_recording_url()]) for s in ss),
'chatlogs': _format_materials((s, s.chatlogs()) for s in ss),
'slides': _format_materials((s, s.slides()) for s in ss),
'drafts': _format_materials((s, s.drafts()) for s in ss),
'last_update': session.last_update if hasattr(session, 'last_update') else None
}
if session and session.meeting.type_id == 'ietf' and not session.meeting.proceedings_final:
entry['attendances'] = _format_materials((s, s) for s in ss if Attended.objects.filter(session=s).exists())
if is_meeting:
meeting_groups.append(entry)
else:
not_meeting_groups.append(entry)
return meeting_groups, not_meeting_groups

View file

@ -20,7 +20,7 @@ from collections import OrderedDict, Counter, deque, defaultdict, namedtuple
from functools import partialmethod
import jsonschema
from pathlib import Path
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit, urlparse
from tempfile import mkstemp
from wsgiref.handlers import format_date_time
@ -75,7 +75,13 @@ from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_
from ietf.meeting.helpers import send_interim_approval
from ietf.meeting.helpers import send_interim_approval_request
from ietf.meeting.helpers import send_interim_announcement_request, sessions_post_cancel
from ietf.meeting.utils import finalize, sort_accept_tuple, condition_slide_order
from ietf.meeting.utils import (
condition_slide_order,
finalize,
generate_proceedings_content,
organize_proceedings_sessions,
sort_accept_tuple,
)
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.utils import session_time_for_sorting
from ietf.meeting.utils import session_requested_by, SaveMaterialsError
@ -86,7 +92,7 @@ from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_ob
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots
from ietf.meeting.utils import preprocess_meeting_important_dates
from ietf.meeting.utils import new_doc_for_session, write_doc_for_session
from ietf.meeting.utils import get_activity_stats, post_process, create_recording
from ietf.meeting.utils import get_activity_stats, post_process, create_recording, delete_recording
from ietf.meeting.utils import participants_for_meeting, generate_bluesheet, bluesheet_data, save_bluesheet
from ietf.message.utils import infer_message
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
@ -103,6 +109,7 @@ from ietf.utils.pdf import pdf_pages
from ietf.utils.response import permission_denied
from ietf.utils.text import xslugify
from ietf.utils.timezone import datetime_today, date_today
from ietf.settings import YOUTUBE_DOMAINS
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm,
@ -2568,6 +2575,89 @@ def add_session_drafts(request, session_id, num):
'form': form,
})
class SessionRecordingsForm(forms.Form):
title = forms.CharField(max_length=255)
url = forms.URLField(label="URL of the recording (YouTube only)")
def clean_url(self):
url = self.cleaned_data['url']
parsed_url = urlparse(url)
if parsed_url.hostname not in YOUTUBE_DOMAINS:
raise forms.ValidationError("Must be a YouTube URL")
return url
def add_session_recordings(request, session_id, num):
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
session = get_object_or_404(Session, pk=session_id)
if not session.can_manage_materials(request.user):
permission_denied(
request, "You don't have permission to manage recordings for this session."
)
if session.is_material_submission_cutoff() and not has_role(
request.user, "Secretariat"
):
raise Http404
session_number = None
official_timeslotassignment = session.official_timeslotassignment()
assertion("official_timeslotassignment is not None")
initial = {
"title": "Video recording of {acronym} for {timestamp}".format(
acronym=session.group.acronym,
timestamp=official_timeslotassignment.timeslot.utc_start_time().strftime(
"%Y-%m-%d %H:%M"
),
)
}
# find session number if WG has more than one session at the meeting
sessions = get_sessions(session.meeting.number, session.group.acronym)
if len(sessions) > 1:
session_number = 1 + sessions.index(session)
presentations = session.presentations.filter(
document__in=session.get_material("recording", only_one=False),
).order_by("document__title", "document__external_url")
if request.method == "POST":
pk_to_delete = request.POST.get("delete", None)
if pk_to_delete is not None:
session_presentation = get_object_or_404(presentations, pk=pk_to_delete)
try:
delete_recording(session_presentation)
except ValueError as err:
log(f"Error deleting recording from session {session.pk}: {err}")
messages.error(
request,
"Unable to delete this recording. Please contact the secretariat for assistance.",
)
form = SessionRecordingsForm(initial=initial)
else:
form = SessionRecordingsForm(request.POST)
if form.is_valid():
title = form.cleaned_data["title"]
url = form.cleaned_data["url"]
create_recording(session, url, title=title, user=request.user.person)
return redirect(
"ietf.meeting.views.session_details",
num=session.meeting.number,
acronym=session.group.acronym,
)
else:
form = SessionRecordingsForm(initial=initial)
return render(
request,
"meeting/add_session_recordings.html",
{
"session": session,
"session_number": session_number,
"already_linked": presentations,
"form": form,
},
)
def session_attendance(request, session_id, num):
"""Session attendance view
@ -4044,93 +4134,10 @@ def upcoming_json(request):
response = HttpResponse(json.dumps(data, indent=2, sort_keys=False), content_type='application/json;charset=%s'%settings.DEFAULT_CHARSET)
return response
def organize_proceedings_sessions(sessions):
# Collect sessions by Group, then bin by session name (including sessions with blank names).
# If all of a group's sessions are 'notmeet', the processed data goes in not_meeting_sessions.
# Otherwise, the data goes in meeting_sessions.
meeting_groups = []
not_meeting_groups = []
for group_acronym, group_sessions in itertools.groupby(sessions, key=lambda s: s.group.acronym):
by_name = {}
is_meeting = False
all_canceled = True
group = None
for s in sorted(
group_sessions,
key=lambda gs: (
gs.official_timeslotassignment().timeslot.time
if gs.official_timeslotassignment() else datetime.datetime(datetime.MAXYEAR, 1, 1)
),
):
group = s.group
if s.current_status != 'notmeet':
is_meeting = True
if s.current_status != 'canceled':
all_canceled = False
by_name.setdefault(s.name, [])
if s.current_status != 'notmeet' or s.presentations.exists():
by_name[s.name].append(s) # for notmeet, only include sessions with materials
for sess_name, ss in by_name.items():
session = ss[0] if ss else None
def _format_materials(items):
"""Format session/material for template
Input is a list of (session, materials) pairs. The materials value can be a single value or a list.
"""
material_times = {} # key is material, value is first timestamp it appeared
for s, mats in items:
tsa = s.official_timeslotassignment()
timestamp = tsa.timeslot.time if tsa else None
if not isinstance(mats, list):
mats = [mats]
for mat in mats:
if mat and mat not in material_times:
material_times[mat] = timestamp
n_mats = len(material_times)
result = []
if n_mats == 1:
result.append({'material': list(material_times)[0]}) # no 'time' when only a single material
elif n_mats > 1:
for mat, timestamp in material_times.items():
result.append({'material': mat, 'time': timestamp})
return result
entry = {
'group': group,
'name': sess_name,
'session': session,
'canceled': all_canceled,
'has_materials': s.presentations.exists(),
'agendas': _format_materials((s, s.agenda()) for s in ss),
'minutes': _format_materials((s, s.minutes()) for s in ss),
'bluesheets': _format_materials((s, s.bluesheets()) for s in ss),
'recordings': _format_materials((s, s.recordings()) for s in ss),
'meetecho_recordings': _format_materials((s, [s.session_recording_url()]) for s in ss),
'chatlogs': _format_materials((s, s.chatlogs()) for s in ss),
'slides': _format_materials((s, s.slides()) for s in ss),
'drafts': _format_materials((s, s.drafts()) for s in ss),
'last_update': session.last_update if hasattr(session, 'last_update') else None
}
if session and session.meeting.type_id == 'ietf' and not session.meeting.proceedings_final:
entry['attendances'] = _format_materials((s, s) for s in ss if Attended.objects.filter(session=s).exists())
if is_meeting:
meeting_groups.append(entry)
else:
not_meeting_groups.append(entry)
return meeting_groups, not_meeting_groups
def proceedings(request, num=None):
def area_and_group_acronyms_from_session(s):
area = s.group_parent_at_the_time()
if area == None:
area = s.group.parent
group = s.group_at_the_time()
return (area.acronym, group.acronym)
meeting = get_meeting(num)
# Early proceedings were hosted on www.ietf.org rather than the datatracker
if meeting.proceedings_format_version == 1:
return HttpResponseRedirect(settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting))
@ -4141,72 +4148,12 @@ def proceedings(request, num=None):
kwargs['num'] = num
return redirect('ietf.meeting.views.materials', **kwargs)
begin_date = meeting.get_submission_start_date()
cut_off_date = meeting.get_submission_cut_off_date()
cor_cut_off_date = meeting.get_submission_correction_date()
today_utc = date_today(datetime.timezone.utc)
schedule = get_schedule(meeting, None)
sessions = (
meeting.session_set.with_current_status()
.filter(Q(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None])
| Q(current_status='notmeet'))
.select_related()
.order_by('-current_status')
)
plenaries, _ = organize_proceedings_sessions(
sessions.filter(name__icontains='plenary')
.exclude(current_status='notmeet')
)
irtf_meeting, irtf_not_meeting = organize_proceedings_sessions(
sessions.filter(group__parent__acronym = 'irtf').order_by('group__acronym')
)
# per Colin (datatracker #5010) - don't report not meeting rags
irtf_not_meeting = [item for item in irtf_not_meeting if item["group"].type_id != "rag"]
irtf = {"meeting_groups":irtf_meeting, "not_meeting_groups":irtf_not_meeting}
training, _ = organize_proceedings_sessions(
sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other',])
.exclude(current_status='notmeet')
)
iab, _ = organize_proceedings_sessions(
sessions.filter(group__parent__acronym = 'iab')
.exclude(current_status='notmeet')
)
editorial, _ = organize_proceedings_sessions(
sessions.filter(group__acronym__in=['rsab','rswg'])
.exclude(current_status='notmeet')
)
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym__in=['edu','iepg','tools'])
ietf = list(ietf)
ietf.sort(key=lambda s: area_and_group_acronyms_from_session(s))
ietf_areas = []
for area, area_sessions in itertools.groupby(ietf, key=lambda s: s.group_parent_at_the_time()):
meeting_groups, not_meeting_groups = organize_proceedings_sessions(area_sessions)
ietf_areas.append((area, meeting_groups, not_meeting_groups))
cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"]
with timezone.override(meeting.tz()):
return render(request, "meeting/proceedings.html", {
return render(request, "meeting/proceedings_wrapper.html", {
'meeting': meeting,
'plenaries': plenaries,
'training': training,
'irtf': irtf,
'iab': iab,
'editorial': editorial,
'ietf_areas': ietf_areas,
'cut_off_date': cut_off_date,
'cor_cut_off_date': cor_cut_off_date,
'submission_started': today_utc > begin_date,
'cache_version': cache_version,
'attendance': meeting.get_attendance(),
'meetinghost_logo': {
'max_height': settings.MEETINGHOST_LOGO_MAX_DISPLAY_HEIGHT,
'max_width': settings.MEETINGHOST_LOGO_MAX_DISPLAY_WIDTH,
}
'proceedings_content': generate_proceedings_content(meeting),
})
@role_required('Secretariat')

View file

@ -1397,3 +1397,6 @@ if SERVER_MODE != 'production':
CSRF_TRUSTED_ORIGINS += ['http://localhost:8000', 'http://127.0.0.1:8000', 'http://[::1]:8000']
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_SAMESITE = 'Lax'
YOUTUBE_DOMAINS = ['www.youtube.com', 'youtube.com', 'youtu.be', 'm.youtube.com', 'youtube-nocookie.com', 'www.youtube-nocookie.com']

View file

@ -0,0 +1,30 @@
// Copyright The IETF Trust 2024-2025, All Rights Reserved
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('delete_recordings_form')
const dialog = document.getElementById('delete_confirm_dialog')
const dialog_link = document.getElementById('delete_confirm_link')
const dialog_submit = document.getElementById('delete_confirm_submit')
const dialog_cancel = document.getElementById('delete_confirm_cancel')
dialog.style.maxWidth = '30vw'
form.addEventListener('submit', (e) => {
e.preventDefault()
dialog_submit.value = e.submitter.value
const recording_link = e.submitter.closest('tr').querySelector('a')
dialog_link.setAttribute('href', recording_link.getAttribute('href'))
dialog_link.textContent = recording_link.textContent
dialog.showModal()
})
dialog_cancel.addEventListener('click', (e) => {
e.preventDefault()
dialog.close()
})
document.addEventListener('keydown', (e) => {
if (dialog.open && e.key === 'Escape') {
dialog.close()
}
})
})

View file

@ -0,0 +1,78 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin static django_bootstrap5 %}
{% block title %}Add I-Ds to {{ session.meeting }} : {{ session.group.acronym }}{% endblock %}
{% block pagehead %}{{ form.media.css }}{% endblock %}
{% block content %}
{% origin %}
<h1>
Add Recordings to {{ session.meeting }}
{% if session_number %}: Session {{ session_number }}{% endif %}
<br>
<small class="text-body-secondary">{{ session.group.acronym }}
{% if session.name %}: {{ session.name }}{% endif %}
</small>
</h1>
{% if session.is_material_submission_cutoff %}
<div class="alert alert-warning my-3">
The deadline for submission corrections has passed. This may affect published proceedings.
</div>
{% endif %}
{% if already_linked|length > 0 %}
<h2 class="mt-5">Recordings already linked to this session</h2>
<form method="post" id="delete_recordings_form">
{% csrf_token %}
<table class="table table-sm table-striped">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">URL</th>
<th scope="col">Delete</th>
</tr>
</thead>
<tbody>
{% for sp in already_linked %}{% with recording_doc=sp.document %}
<tr>
<td>{{ recording_doc.title }}</td>
<td><a href="{{ recording_doc.external_url }}">{{ recording_doc.external_url }}</a></td>
<td>
<button type="submit"
aria-label="Delete {{ recording_doc.title }}"
class="btn btn-danger"
name="delete"
value="{{ sp.pk }}">
Delete
</button>
</td>
</tr>
{% endwith %}{% endfor %}
</tbody>
</table>
</form>
{% endif %}
<dialog id="delete_confirm_dialog">
<p>Really delete the link to <a href="#" id="delete_confirm_link">(default)</a>?</p>
<form method="post" class="d-flex justify-content-between">
{% csrf_token %}
<button class="btn btn-secondary" type="button" id="delete_confirm_cancel">Cancel</button>
<button class="btn btn-danger" type="submit" name="delete" id="delete_confirm_submit">Delete</button>
</form>
</dialog>
<h2 class="mt-5">Add a recording to this session</h2>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<button class="btn btn-{% if session.is_material_submission_cutoff %}warning{% else %}primary{% endif %}"
type="submit">
Add recording
</button>
<a class="btn btn-secondary float-end"
href="{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}">
Back
</a>
</form>
{% endblock %}
{% block js %}
{{ form.media.js }}
<script src="{% static 'ietf/js/add_session_recordings.js' %}"></script>
{% endblock %}

View file

@ -1,184 +1,160 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load ietf_filters static %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
{% endblock %}
{% block title %}
IETF {{ meeting.number }}
{% if not meeting.proceedings_final %}Draft{% endif %}
Proceedings
{% endblock %}
{% block content %}
{% origin %}
{% include 'meeting/proceedings/title.html' with meeting=meeting attendance=attendance only %}
{% if user|has_role:"Secretariat" and not meeting.proceedings_final %}
<a class="btn btn-warning finalize-button"
href="{% url 'ietf.meeting.views.finalize_proceedings' num=meeting.number %}">
Finalize proceedings
</a>
{% endif %}
{# cache for 15 minutes, as long as there's no proceedings activity. takes 4-8 seconds to generate. #}
{% load cache %}
{% cache 900 ietf_meeting_proceedings meeting.number cache_version %}
{% include 'meeting/proceedings/introduction.html' with meeting=meeting only %}
<!-- Plenaries -->
{% if plenaries %}
<h2 class="mt-5" id="plenaries">Plenaries</h2>
{% include 'meeting/proceedings/introduction.html' with meeting=meeting only %}
<!-- Plenaries -->
{% if plenaries %}
<h2 class="mt-5" id="plenaries">Plenaries</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Internet-Drafts</th>
</tr>
</thead>
<tbody>
{% for entry in plenaries %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- Working groups -->
{% for area, meeting_groups, not_meeting_groups in ietf_areas %}
<h2 class="mt-5" id="{{ area.acronym }}">
{{ area.acronym|upper }} <small class="text-body-secondary">{{ area.name }}</small>
</h2>
{% if meeting_groups %}
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Internet-Drafts</th>
</tr>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Internet-Drafts</th>
</tr>
</thead>
<tbody>
{% for entry in plenaries %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
{% for entry in meeting_groups %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- Working groups -->
{% for area, meeting_groups, not_meeting_groups in ietf_areas %}
<h2 class="mt-5" id="{{ area.acronym }}">
{{ area.acronym|upper }} <small class="text-body-secondary">{{ area.name }}</small>
</h2>
{% if meeting_groups %}
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Internet-Drafts</th>
</tr>
</thead>
<tbody>
{% for entry in meeting_groups %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% if not_meeting_groups %}
<p>
{{ area.name }} groups not meeting:
{% for entry in not_meeting_groups %}
{% if entry.name == "" %}{# do not show named sessions in this list #}
<a href="{% url 'ietf.group.views.group_home' acronym=entry.group.acronym %}">
{{ entry.group.acronym }}
</a>{% if not forloop.last %},{% endif %}
{% endif %}
{% endfor %}
</p>
<table class="table table-sm table-striped">
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for entry in not_meeting_groups %}{% if entry.has_materials %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endif %}{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
<!-- Training Sessions -->
{% if training %}
<h2 class="mt-5" id="training">Training</h2>
<table class="table table-sm table-striped tablesorter">
{% if not_meeting_groups %}
<p>
{{ area.name }} groups not meeting:
{% for entry in not_meeting_groups %}
{% if entry.name == "" %}{# do not show named sessions in this list #}
<a href="{% url 'ietf.group.views.group_home' acronym=entry.group.acronym %}">
{{ entry.group.acronym }}
</a>{% if not forloop.last %},{% endif %}
{% endif %}
{% endfor %}
</p>
<table class="table table-sm table-striped">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Internet-Drafts</th>
</tr>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for entry in training %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=False only %}
{% endfor %}
{% for entry in not_meeting_groups %}{% if entry.has_materials %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endif %}{% endfor %}
</tbody>
</table>
{% endif %}
<!-- IAB Sessions -->
{% if iab %}
<h2 class="mt-5" id="iab">
IAB <small class="text-body-secondary">Internet Architecture Board</small>
</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Internet-Drafts
</th>
</tr>
</thead>
<tbody>
{% for entry in iab %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- IRTF Sessions -->
{% if irtf.meeting_groups %}
<h2 class="mt-5" id="irtf">
IRTF <small class="text-body-secondary">Internet Research Task Force</small>
</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Internet-Drafts
</th>
</tr>
</thead>
<tbody>
{% for entry in irtf.meeting_groups %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% if irtf.not_meeting_groups %}
{% endfor %}
<!-- Training Sessions -->
{% if training %}
<h2 class="mt-5" id="training">Training</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">Group</th>
<th scope="col" data-sort="artifacts">Artifacts</th>
<th scope="col" data-sort="recordings">Recordings</th>
<th scope="col" data-sort="slides">Slides</th>
<th scope="col" data-sort="drafts">Internet-Drafts</th>
</tr>
</thead>
<tbody>
{% for entry in training %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=False only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- IAB Sessions -->
{% if iab %}
<h2 class="mt-5" id="iab">
IAB <small class="text-body-secondary">Internet Architecture Board</small>
</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Internet-Drafts
</th>
</tr>
</thead>
<tbody>
{% for entry in iab %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
<!-- IRTF Sessions -->
{% if irtf.meeting_groups %}
<h2 class="mt-5" id="irtf">
IRTF <small class="text-body-secondary">Internet Research Task Force</small>
</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Internet-Drafts
</th>
</tr>
</thead>
<tbody>
{% for entry in irtf.meeting_groups %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% if irtf.not_meeting_groups %}
<p>
IRTF groups not meeting:
{% for entry in irtf.not_meeting_groups %}
@ -191,18 +167,18 @@
</p>
<table class="table table-sm table-striped">
<thead>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
<tr>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for entry in irtf.not_meeting %}{% if entry.has_materials %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endif %}{% endfor %}
{% for entry in irtf.not_meeting %}{% if entry.has_materials %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endif %}{% endfor %}
</tbody>
</table>
{% endif %}
@ -211,35 +187,29 @@
<h2 class="mt-5" id="editorial">Editorial Stream</h2>
<table class="table table-sm table-striped tablesorter">
<thead>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Internet-Drafts
</th>
</tr>
<tr>
<th scope="col" data-sort="group">
Group
</th>
<th scope="col" data-sort="artifacts">
Artifacts
</th>
<th scope="col" data-sort="recordings">
Recordings
</th>
<th scope="col" data-sort="slides">
Slides
</th>
<th scope="col" data-sort="drafts">
Internet-Drafts
</th>
</tr>
</thead>
<tbody>
{% for entry in editorial %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
{% for entry in editorial %}
{% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% endif %}
{% endcache %}
{% endblock %}
{% block js %}
<script src="{% static "ietf/js/list.js" %}">
</script>
{% endblock %}
{% endif %}

View file

@ -0,0 +1,27 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load ietf_filters static %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
{% endblock %}
{% block title %}
IETF {{ meeting.number }}
{% if not meeting.proceedings_final %}Draft{% endif %}
Proceedings
{% endblock %}
{% block content %}
{% origin %}
{% include 'meeting/proceedings/title.html' with meeting=meeting attendance=attendance only %}
{% if user|has_role:"Secretariat" and not meeting.proceedings_final %}
<a class="btn btn-warning finalize-button"
href="{% url 'ietf.meeting.views.finalize_proceedings' num=meeting.number %}">
Finalize proceedings
</a>
{% endif %}
{{ proceedings_content }}
{% endblock %}
{% block js %}
<script src="{% static "ietf/js/list.js" %}">
</script>
{% endblock %}

View file

@ -368,5 +368,13 @@
</tbody>
</table>
{% endif %}
{% if can_manage_materials %}
<a class="btn btn-primary"
href="{% url 'ietf.meeting.views.add_session_recordings' session_id=session.pk num=session.meeting.number %}">
Link additional recordings to session
</a>
{% endif %}
{% endwith %}{% endwith %}
{% endfor %}

View file

@ -111,6 +111,7 @@
"ietf/static/images/irtf-logo-card.png",
"ietf/static/images/irtf-logo-white.svg",
"ietf/static/images/irtf-logo.svg",
"ietf/static/js/add_session_recordings.js",
"ietf/static/js/agenda_filter.js",
"ietf/static/js/agenda_materials.js",
"ietf/static/js/complete-review.js",