diff --git a/dev/deploy-to-container/package-lock.json b/dev/deploy-to-container/package-lock.json index 86cb1f732..03327083b 100644 --- a/dev/deploy-to-container/package-lock.json +++ b/dev/deploy-to-container/package-lock.json @@ -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", diff --git a/dev/deploy-to-container/package.json b/dev/deploy-to-container/package.json index 53a362b9d..1f54745eb 100644 --- a/dev/deploy-to-container/package.json +++ b/dev/deploy-to-container/package.json @@ -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", diff --git a/dev/diff/package-lock.json b/dev/diff/package-lock.json index 87aaf3f16..4e92d2a6e 100644 --- a/dev/diff/package-lock.json +++ b/dev/diff/package-lock.json @@ -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", diff --git a/dev/diff/package.json b/dev/diff/package.json index 4e0e1be8f..875e242d3 100644 --- a/dev/diff/package.json +++ b/dev/diff/package.json @@ -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", diff --git a/docker-compose.yml b/docker-compose.yml index 65b28f54f..9988b10c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -101,6 +101,7 @@ services: # stop_grace_period: 1m # volumes: # - .:/workspace +# - app-assets:/assets volumes: postgresdb-data: diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py index 50fba50c4..19aa9ecc9 100644 --- a/ietf/doc/factories.py +++ b/ietf/doc/factories.py @@ -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 diff --git a/ietf/ipr/management/commands/generate_draft_recursive_txt.py b/ietf/ipr/management/commands/generate_draft_recursive_txt.py deleted file mode 100644 index 2c61a7604..000000000 --- a/ietf/ipr/management/commands/generate_draft_recursive_txt.py +++ /dev/null @@ -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) diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 67be278db..e0d00b5d1 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -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) diff --git a/ietf/ipr/urls.py b/ietf/ipr/urls.py index 2b6abee31..84ed04a66 100644 --- a/ietf/ipr/urls.py +++ b/ietf/ipr/urls.py @@ -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/(?Ppending|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\d+)/$', views.show), url(r'^(?P\d+)/addcomment/$', views.add_comment), url(r'^(?P\d+)/addemail/$', views.add_email), diff --git a/ietf/ipr/utils.py b/ietf/ipr/utils.py index 8f0b9cf3f..7e569a1d1 100644 --- a/ietf/ipr/utils.py +++ b/ietf/ipr/utils.py @@ -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 diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 0347c4d78..24453df2d 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -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 diff --git a/ietf/meeting/tasks.py b/ietf/meeting/tasks.py index 43cbb0a75..2b7c2fca9 100644 --- a/ietf/meeting/tasks.py +++ b/ietf/meeting/tasks.py @@ -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) diff --git a/ietf/meeting/tests_tasks.py b/ietf/meeting/tests_tasks.py new file mode 100644 index 000000000..c026a9983 --- /dev/null +++ b/ietf/meeting/tests_tasks.py @@ -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) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index b52f8de9b..581725dbc 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -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("

Fake proceedings content

") + 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""" diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index 42a5de623..5da24ddb6 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -16,6 +16,7 @@ class AgendaRedirectView(RedirectView): safe_for_all_meeting_types = [ url(r'^session/(?P[-a-z0-9]+)/?$', views.session_details), url(r'^session/(?P\d+)/drafts$', views.add_session_drafts), + url(r'^session/(?P\d+)/recordings$', views.add_session_recordings), url(r'^session/(?P\d+)/attendance$', views.session_attendance), url(r'^session/(?P\d+)/bluesheets$', views.upload_session_bluesheets), url(r'^session/(?P\d+)/minutes$', views.upload_session_minutes), @@ -63,7 +64,8 @@ type_ietf_only_patterns = [ type_interim_patterns = [ url(r'^agenda/(?P[A-Za-z0-9-]+)-drafts.pdf$', views.session_draft_pdf), url(r'^agenda/(?P[A-Za-z0-9-]+)-drafts.tgz$', views.session_draft_tarfile), - url(r'^materials/%(document)s((?P\.[a-z0-9]+)|/)?$' % settings.URL_REGEXPS, views.materials_document), + url(r'^materials/%(document)s(?P\.[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) ] diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 4f800980c..92bae5ac2 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -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 diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index feb14f730..1226e30d6 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -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') diff --git a/ietf/settings.py b/ietf/settings.py index 7fd4308bb..125127ba1 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -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'] diff --git a/ietf/static/js/add_session_recordings.js b/ietf/static/js/add_session_recordings.js new file mode 100644 index 000000000..c1c5932a4 --- /dev/null +++ b/ietf/static/js/add_session_recordings.js @@ -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() + } + }) +}) diff --git a/ietf/templates/meeting/add_session_recordings.html b/ietf/templates/meeting/add_session_recordings.html new file mode 100644 index 000000000..4f21e8f4d --- /dev/null +++ b/ietf/templates/meeting/add_session_recordings.html @@ -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 %} +

+ Add Recordings to {{ session.meeting }} + {% if session_number %}: Session {{ session_number }}{% endif %} +
+ {{ session.group.acronym }} + {% if session.name %}: {{ session.name }}{% endif %} + +

+ {% if session.is_material_submission_cutoff %} +
+ The deadline for submission corrections has passed. This may affect published proceedings. +
+ {% endif %} + {% if already_linked|length > 0 %} +

Recordings already linked to this session

+
+ {% csrf_token %} + + + + + + + + + + {% for sp in already_linked %}{% with recording_doc=sp.document %} + + + + + + {% endwith %}{% endfor %} + +
TitleURLDelete
{{ recording_doc.title }}{{ recording_doc.external_url }} + +
+
+ {% endif %} + +

Really delete the link to (default)?

+
+ {% csrf_token %} + + +
+
+

Add a recording to this session

+
+ {% csrf_token %} + {% bootstrap_form form %} + + + Back + +
+{% endblock %} +{% block js %} + {{ form.media.js }} + +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/meeting/proceedings.html b/ietf/templates/meeting/proceedings.html index b5d4a6198..0aa8197fe 100644 --- a/ietf/templates/meeting/proceedings.html +++ b/ietf/templates/meeting/proceedings.html @@ -1,184 +1,160 @@ -{% extends "base.html" %} -{# Copyright The IETF Trust 2015, All Rights Reserved #} -{% load origin %} -{% load ietf_filters static %} -{% block pagehead %} - -{% 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 %} - - Finalize proceedings - - {% 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 %} - - {% if plenaries %} -

Plenaries

+{% include 'meeting/proceedings/introduction.html' with meeting=meeting only %} + +{% if plenaries %} +

Plenaries

+ + + + + + + + + + + + {% for entry in plenaries %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} + {% endfor %} + +
GroupArtifactsRecordingsSlidesInternet-Drafts
+{% endif %} + +{% for area, meeting_groups, not_meeting_groups in ietf_areas %} +

+ {{ area.acronym|upper }} {{ area.name }} +

+ {% if meeting_groups %} - - - - - - - + + + + + + + - {% 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 %}
GroupArtifactsRecordingsSlidesInternet-Drafts
GroupArtifactsRecordingsSlidesInternet-Drafts
{% endif %} - - {% for area, meeting_groups, not_meeting_groups in ietf_areas %} -

- {{ area.acronym|upper }} {{ area.name }} -

- {% if meeting_groups %} - - - - - - - - - - - - {% for entry in meeting_groups %} - {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} - {% endfor %} - -
GroupArtifactsRecordingsSlidesInternet-Drafts
- {% endif %} - {% if not_meeting_groups %} -

- {{ area.name }} groups not meeting: - {% for entry in not_meeting_groups %} - {% if entry.name == "" %}{# do not show named sessions in this list #} - - {{ entry.group.acronym }} - {% if not forloop.last %},{% endif %} - {% endif %} - {% 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 %} - -
- {% endif %} - {% endfor %} - - {% if training %} -

Training

- + {% if not_meeting_groups %} +

+ {{ area.name }} groups not meeting: + {% for entry in not_meeting_groups %} + {% if entry.name == "" %}{# do not show named sessions in this list #} + + {{ entry.group.acronym }} + {% if not forloop.last %},{% endif %} + {% endif %} + {% endfor %} +

+
- - - - - - - + + + + + + + - {% 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 %}
GroupArtifactsRecordingsSlidesInternet-Drafts
{% endif %} - - {% if iab %} -

- IAB Internet Architecture Board -

- - - - - - - - - - - - {% for entry in iab %} - {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} - {% endfor %} - -
- Group - - Artifacts - - Recordings - - Slides - - Internet-Drafts -
- {% endif %} - - {% if irtf.meeting_groups %} -

- IRTF Internet Research Task Force -

- - - - - - - - - - - - {% for entry in irtf.meeting_groups %} - {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} - {% endfor %} - -
- Group - - Artifacts - - Recordings - - Slides - - Internet-Drafts -
- {% if irtf.not_meeting_groups %} +{% endfor %} + +{% if training %} +

Training

+ + + + + + + + + + + + {% for entry in training %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=False only %} + {% endfor %} + +
GroupArtifactsRecordingsSlidesInternet-Drafts
+{% endif %} + +{% if iab %} +

+ IAB Internet Architecture Board +

+ + + + + + + + + + + + {% for entry in iab %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} + {% endfor %} + +
+ Group + + Artifacts + + Recordings + + Slides + + Internet-Drafts +
+{% endif %} + +{% if irtf.meeting_groups %} +

+ IRTF Internet Research Task Force +

+ + + + + + + + + + + + {% for entry in irtf.meeting_groups %} + {% include "meeting/group_proceedings.html" with entry=entry meeting=meeting show_agenda=True only %} + {% endfor %} + +
+ Group + + Artifacts + + Recordings + + Slides + + Internet-Drafts +
+ {% if irtf.not_meeting_groups %}

IRTF groups not meeting: {% for entry in irtf.not_meeting_groups %} @@ -191,18 +167,18 @@

- - - - - - - + + + + + + + - {% 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 %}
{% endif %} @@ -211,35 +187,29 @@

Editorial Stream

- - - - - - - + + + + + + + - {% 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 %}
- Group - - Artifacts - - Recordings - - Slides - - Internet-Drafts -
+ Group + + Artifacts + + Recordings + + Slides + + Internet-Drafts +
{% endif %} - {% endif %} -{% endcache %} -{% endblock %} -{% block js %} - -{% endblock %} +{% endif %} diff --git a/ietf/templates/meeting/proceedings_wrapper.html b/ietf/templates/meeting/proceedings_wrapper.html new file mode 100644 index 000000000..a20291a69 --- /dev/null +++ b/ietf/templates/meeting/proceedings_wrapper.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} +{% load ietf_filters static %} +{% block pagehead %} + +{% 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 %} + + Finalize proceedings + + {% endif %} + {{ proceedings_content }} +{% endblock %} +{% block js %} + +{% endblock %} diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index a0f5884b9..9b7a192f0 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -368,5 +368,13 @@ {% endif %} + + {% if can_manage_materials %} + + Link additional recordings to session + + {% endif %} + {% endwith %}{% endwith %} {% endfor %} diff --git a/package.json b/package.json index b3d36b349..6d1591ca1 100644 --- a/package.json +++ b/package.json @@ -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",