diff --git a/local.env.dist b/local.env.dist
index 6b7efc82..74cd1d31 100644
--- a/local.env.dist
+++ b/local.env.dist
@@ -22,6 +22,7 @@ BUILD_ENGINE_SECRETS_BUCKET=SOME_ORG_PREFIX-SOME_APP_ENV-aps-secrets
# Built artifacts will be stored here
BUILD_ENGINE_ARTIFACTS_BUCKET=SOME_ORG_PREFIX-SOME_APP_ENV-aps-artifacts
BUILD_ENGINE_ARTIFACTS_BUCKET_REGION=us-east-1
+BUILD_ENGINE_GRADING_LAMBDA_FUNCTION_NAME=SOME_GRADING_LAMBDA_FUNCTION
# Projects are moved to here by SAB
BUILD_ENGINE_PROJECTS_BUCKET=SOME_ORG_PREFIX-SOME_APP_ENV-aps-projects
diff --git a/package-lock.json b/package-lock.json
index 6ed98fd4..b6b9ac00 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@aws-sdk/client-codebuild": "^3.907.0",
"@aws-sdk/client-ecr": "^3.981.0",
"@aws-sdk/client-iam": "^3.911.0",
+ "@aws-sdk/client-lambda": "^3.907.0",
"@aws-sdk/client-s3": "^3.907.0",
"@aws-sdk/client-sts": "^3.907.0",
"@grpc/grpc-js": "^1.14.3",
@@ -1497,6 +1498,67 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/@aws-sdk/client-lambda": {
+ "version": "3.907.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.907.0.tgz",
+ "integrity": "sha512-Xj5vlaWbYKzvE3r+lgBSZhV6XtGGBEMQEor4iez7FWI9jKbA/3saGXBXVj+mrpMAAVk3IPLzwdN/7drY4Ft7HA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.907.0",
+ "@aws-sdk/credential-provider-node": "3.907.0",
+ "@aws-sdk/middleware-host-header": "3.901.0",
+ "@aws-sdk/middleware-logger": "3.901.0",
+ "@aws-sdk/middleware-recursion-detection": "3.901.0",
+ "@aws-sdk/middleware-user-agent": "3.907.0",
+ "@aws-sdk/region-config-resolver": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/util-endpoints": "3.901.0",
+ "@aws-sdk/util-user-agent-browser": "3.907.0",
+ "@aws-sdk/util-user-agent-node": "3.907.0",
+ "@smithy/config-resolver": "^4.3.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/eventstream-serde-browser": "^4.2.0",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.0",
+ "@smithy/eventstream-serde-node": "^4.2.0",
+ "@smithy/fetch-http-handler": "^5.3.0",
+ "@smithy/hash-node": "^4.2.0",
+ "@smithy/invalid-dependency": "^4.2.0",
+ "@smithy/middleware-content-length": "^4.2.0",
+ "@smithy/middleware-endpoint": "^4.3.0",
+ "@smithy/middleware-retry": "^4.4.0",
+ "@smithy/middleware-serde": "^4.2.0",
+ "@smithy/middleware-stack": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/node-http-handler": "^4.3.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/url-parser": "^4.2.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.0",
+ "@smithy/util-defaults-mode-browser": "^4.2.0",
+ "@smithy/util-defaults-mode-node": "^4.2.0",
+ "@smithy/util-endpoints": "^3.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-retry": "^4.2.0",
+ "@smithy/util-stream": "^4.4.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/util-waiter": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-lambda/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"node_modules/@aws-sdk/client-s3": {
"version": "3.907.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.907.0.tgz",
@@ -1792,18 +1854,16 @@
"license": "0BSD"
},
"node_modules/@aws-sdk/credential-provider-login": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.3.tgz",
- "integrity": "sha512-Gc3O91iVvA47kp2CLIXOwuo5ffo1cIpmmyIewcYjAcvurdFHQ8YdcBe1KHidnbbBO4/ZtywGBACsAX5vr3UdoA==",
+ "version": "3.972.43",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.43.tgz",
+ "integrity": "sha512-HG7kQCwXtbv3oBV61Ins0oNX8KKyvrMqqRkb6ZiAfQHbMuHaiNaEb2KnpKLPkNpqImSBK82UkVE/kaY6IfWikA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/nested-clients": "3.980.0",
- "@aws-sdk/types": "^3.973.1",
- "@smithy/property-provider": "^4.2.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/shared-ini-file-loader": "^4.4.3",
- "@smithy/types": "^4.12.0",
+ "@aws-sdk/core": "^3.974.13",
+ "@aws-sdk/nested-clients": "^3.997.11",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.3",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -1811,86 +1871,18 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/core": {
- "version": "3.973.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.5.tgz",
- "integrity": "sha512-IMM7xGfLGW6lMvubsA4j6BHU5FPgGAxoQ/NA63KqNLMwTS+PeMBcx8DPHL12Vg6yqOZnqok9Mu4H2BdQyq7gSA==",
+ "version": "3.974.13",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.13.tgz",
+ "integrity": "sha512-+Y5/4tHki0uYgyx8eun146DegRVQBpdKGK5RbV0FTKJPpaKTchvqVxrrRFK6Wk0JksO4iAZKw3eqxGEIwtO98w==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@aws-sdk/xml-builder": "^3.972.2",
- "@smithy/core": "^3.22.0",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/property-provider": "^4.2.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/signature-v4": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
- "@smithy/types": "^4.12.0",
- "@smithy/util-base64": "^4.3.0",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-utf8": "^4.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-host-header": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz",
- "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-logger": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz",
- "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/types": "^4.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-recursion-detection": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz",
- "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/types": "^3.973.9",
+ "@aws-sdk/xml-builder": "^3.972.25",
"@aws/lambda-invoke-store": "^0.2.2",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/middleware-user-agent": {
- "version": "3.972.5",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.5.tgz",
- "integrity": "sha512-TVZQ6PWPwQbahUI8V+Er+gS41ctIawcI/uMNmQtQ7RMcg3JYn6gyKAFKUb3HFYx2OjYlx1u11sETSwwEUxVHTg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
- "@smithy/core": "^3.22.0",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
+ "@smithy/core": "^3.24.3",
+ "@smithy/signature-v4": "^5.4.2",
+ "@smithy/types": "^4.14.2",
+ "bowser": "^2.11.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -1898,64 +1890,36 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/nested-clients": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.980.0.tgz",
- "integrity": "sha512-/dONY5xc5/CCKzOqHZCTidtAR4lJXWkGefXvTRKdSKMGaYbbKsxDckisd6GfnvPSLxWtvQzwgRGRutMRoYUApQ==",
+ "version": "3.997.11",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.11.tgz",
+ "integrity": "sha512-nWXXJ1r/r8N2Gw1pWolRgED38/A9A8DHR2ETWIv220zh4PZHcybbR4hUVWWktmNXTRHzDJwRluapHn0rZxuoqA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.5",
- "@aws-sdk/middleware-host-header": "^3.972.3",
- "@aws-sdk/middleware-logger": "^3.972.3",
- "@aws-sdk/middleware-recursion-detection": "^3.972.3",
- "@aws-sdk/middleware-user-agent": "^3.972.5",
- "@aws-sdk/region-config-resolver": "^3.972.3",
- "@aws-sdk/types": "^3.973.1",
- "@aws-sdk/util-endpoints": "3.980.0",
- "@aws-sdk/util-user-agent-browser": "^3.972.3",
- "@aws-sdk/util-user-agent-node": "^3.972.3",
- "@smithy/config-resolver": "^4.4.6",
- "@smithy/core": "^3.22.0",
- "@smithy/fetch-http-handler": "^5.3.9",
- "@smithy/hash-node": "^4.2.8",
- "@smithy/invalid-dependency": "^4.2.8",
- "@smithy/middleware-content-length": "^4.2.8",
- "@smithy/middleware-endpoint": "^4.4.12",
- "@smithy/middleware-retry": "^4.4.29",
- "@smithy/middleware-serde": "^4.2.9",
- "@smithy/middleware-stack": "^4.2.8",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/node-http-handler": "^4.4.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/smithy-client": "^4.11.1",
- "@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
- "@smithy/util-base64": "^4.3.0",
- "@smithy/util-body-length-browser": "^4.2.0",
- "@smithy/util-body-length-node": "^4.2.1",
- "@smithy/util-defaults-mode-browser": "^4.3.28",
- "@smithy/util-defaults-mode-node": "^4.2.31",
- "@smithy/util-endpoints": "^3.2.8",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-retry": "^4.2.8",
- "@smithy/util-utf8": "^4.2.0",
+ "@aws-sdk/core": "^3.974.13",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.28",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.3",
+ "@smithy/fetch-http-handler": "^5.4.3",
+ "@smithy/node-http-handler": "^4.7.3",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/region-config-resolver": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz",
- "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==",
+ "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.28",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.28.tgz",
+ "integrity": "sha512-qs9z5LqXO/CZC2Lg9SGKpoLU8Rhi+m2pFKZqfO9pytX1clc0katqtsDNupJxFy0xT9wsZSPzM2v1y+/H/zfp5Q==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/config-resolver": "^4.4.6",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/types": "^4.12.0",
+ "@aws-sdk/types": "^3.973.9",
+ "@smithy/core": "^3.24.3",
+ "@smithy/signature-v4": "^5.4.2",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -1963,78 +1927,27 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/types": {
- "version": "3.973.1",
- "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz",
- "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/types": "^4.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.980.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.980.0.tgz",
- "integrity": "sha512-AjKBNEc+rjOZQE1HwcD9aCELqg1GmUj1rtICKuY8cgwB73xJ4U/kNyqKKpN2k9emGqlfDY2D8itIp/vDc6OKpw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
- "@smithy/util-endpoints": "^3.2.8",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=20.0.0"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-browser": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz",
- "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "^3.973.1",
- "@smithy/types": "^4.12.0",
- "bowser": "^2.11.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/util-user-agent-node": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.3.tgz",
- "integrity": "sha512-gqG+02/lXQtO0j3US6EVnxtwwoXQC5l2qkhLCrqUrqdtcQxV7FDMbm9wLjKqoronSHyELGTjbFKK/xV5q1bZNA==",
+ "version": "3.973.9",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz",
+ "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-user-agent": "^3.972.5",
- "@aws-sdk/types": "^3.973.1",
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/types": "^4.12.0",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
- },
- "peerDependencies": {
- "aws-crt": ">=1.0.0"
- },
- "peerDependenciesMeta": {
- "aws-crt": {
- "optional": true
- }
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws-sdk/xml-builder": {
- "version": "3.972.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.3.tgz",
- "integrity": "sha512-bCk63RsBNCWW4tt5atv5Sbrh+3J3e8YzgyF6aZb1JeXcdzG4k5SlPLeTMFOIXFuuFHIwgphUhn4i3uS/q49eww==",
+ "version": "3.972.25",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.25.tgz",
+ "integrity": "sha512-GH+Kjz4nPKWKHnsiQpnhP1MJdTGIcK4rAka6tzakgjjUkVgNsmPeEbbRAf09SzS1hjGu6duGHCBsxYke0BhHjQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.12.0",
- "fast-xml-parser": "5.3.4",
+ "@nodable/entities": "2.1.0",
+ "@smithy/types": "^4.14.2",
+ "fast-xml-parser": "5.7.3",
"tslib": "^2.6.2"
},
"engines": {
@@ -2042,18 +1955,18 @@
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/@aws/lambda-invoke-store": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
- "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==",
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz",
+ "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-login/node_modules/fast-xml-parser": {
- "version": "5.3.4",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz",
- "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==",
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz",
+ "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==",
"funding": [
{
"type": "github",
@@ -2062,7 +1975,10 @@
],
"license": "MIT",
"dependencies": {
- "strnum": "^2.1.0"
+ "@nodable/entities": "^2.1.0",
+ "fast-xml-builder": "^1.1.7",
+ "path-expression-matcher": "^1.5.0",
+ "strnum": "^2.2.3"
},
"bin": {
"fxparser": "src/cli/cli.js"
@@ -3679,6 +3595,18 @@
"win32"
]
},
+ "node_modules/@nodable/entities": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
+ "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodable"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4964,20 +4892,13 @@
"license": "0BSD"
},
"node_modules/@smithy/core": {
- "version": "3.22.1",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz",
- "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==",
+ "version": "3.24.4",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.4.tgz",
+ "integrity": "sha512-3UNRKEyQyAgVgM0LGlerCLm+ChZWZ1GPfde+jBEW6bm6bSBGU1p0EbblaUV3unbhwvidjLA5Zs3sOs7mnZwvAw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/middleware-serde": "^4.2.9",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
- "@smithy/util-base64": "^4.3.0",
- "@smithy/util-body-length-browser": "^4.2.0",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-stream": "^4.5.11",
- "@smithy/util-utf8": "^4.2.0",
- "@smithy/uuid": "^1.1.0",
+ "@aws-crypto/crc32": "5.2.0",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -4991,15 +4912,13 @@
"license": "0BSD"
},
"node_modules/@smithy/credential-provider-imds": {
- "version": "4.2.8",
- "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz",
- "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==",
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.4.tgz",
+ "integrity": "sha512-vKW0MEFRU4Y3MkVZUkpJm+g9qyPGLCXhc0YLggUdSdBB4g7IaSSsCE75P9rBXyWHrXY1UYSQUl8/DwsTR7QciA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.8",
- "@smithy/property-provider": "^4.2.8",
- "@smithy/types": "^4.12.0",
- "@smithy/url-parser": "^4.2.8",
+ "@smithy/core": "^3.24.4",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5113,15 +5032,13 @@
"license": "0BSD"
},
"node_modules/@smithy/fetch-http-handler": {
- "version": "5.3.9",
- "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz",
- "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==",
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.4.tgz",
+ "integrity": "sha512-qM7AUKI4G6d7lNgaZD3lA1tWSolh5r6gcixfTZAPstVURfjIbvreVTPz+994M0yC3HbX4YYhDRgr31Xy3XwWOQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/querystring-builder": "^4.2.8",
- "@smithy/types": "^4.12.0",
- "@smithy/util-base64": "^4.3.0",
+ "@smithy/core": "^3.24.4",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5385,15 +5302,13 @@
"license": "0BSD"
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.4.9",
- "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz",
- "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==",
+ "version": "4.7.4",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.4.tgz",
+ "integrity": "sha512-HIeF+1vrDGzPkkv39Hj2vlHSXHY3p958jd/8ZnePIY6+ZOsQX8coyEUKO5yQu4r0bQIVsbpotVIrXXwyycMStQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/abort-controller": "^4.2.8",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/querystring-builder": "^4.2.8",
- "@smithy/types": "^4.12.0",
+ "@smithy/core": "^3.24.4",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5444,26 +5359,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
- "node_modules/@smithy/querystring-builder": {
- "version": "4.2.8",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz",
- "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/types": "^4.12.0",
- "@smithy/util-uri-escape": "^4.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@smithy/querystring-builder/node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
"node_modules/@smithy/querystring-parser": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz",
@@ -5515,18 +5410,13 @@
"license": "0BSD"
},
"node_modules/@smithy/signature-v4": {
- "version": "5.3.8",
- "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz",
- "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==",
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.4.tgz",
+ "integrity": "sha512-e5UtkMvsatzBfbeBZjEOt0k0Z3BEsjTFL/n6fdO5vtBLe67tdy0dX7xw2DU7uZ3acwoHyeCqpU2Fzb7pxwHb6Q==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/is-array-buffer": "^4.2.0",
- "@smithy/protocol-http": "^5.3.8",
- "@smithy/types": "^4.12.0",
- "@smithy/util-hex-encoding": "^4.2.0",
- "@smithy/util-middleware": "^4.2.8",
- "@smithy/util-uri-escape": "^4.2.0",
- "@smithy/util-utf8": "^4.2.0",
+ "@smithy/core": "^3.24.4",
+ "@smithy/types": "^4.14.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5564,9 +5454,9 @@
"license": "0BSD"
},
"node_modules/@smithy/types": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz",
- "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==",
+ "version": "4.14.2",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz",
+ "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -5841,24 +5731,6 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
- "node_modules/@smithy/util-uri-escape": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz",
- "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@smithy/util-uri-escape/node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
"node_modules/@smithy/util-utf8": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz",
@@ -8301,6 +8173,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-xml-builder": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz",
+ "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "path-expression-matcher": "^1.5.0",
+ "xml-naming": "^0.1.0"
+ }
+ },
"node_modules/fast-xml-parser": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
@@ -10259,6 +10147,21 @@
"node": ">=8"
}
},
+ "node_modules/path-expression-matcher": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
+ "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -11375,9 +11278,9 @@
}
},
"node_modules/strnum": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
- "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
+ "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==",
"funding": [
{
"type": "github",
@@ -12238,6 +12141,21 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/xml-naming": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
+ "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/package.json b/package.json
index 48aaadae..3bd12593 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"@aws-sdk/client-codebuild": "^3.907.0",
"@aws-sdk/client-ecr": "^3.981.0",
"@aws-sdk/client-iam": "^3.911.0",
+ "@aws-sdk/client-lambda": "^3.907.0",
"@aws-sdk/client-s3": "^3.907.0",
"@aws-sdk/client-sts": "^3.907.0",
"@grpc/grpc-js": "^1.14.3",
diff --git a/src/lib/prisma/migrations/03_grading_result/migration.sql b/src/lib/prisma/migrations/03_grading_result/migration.sql
new file mode 100644
index 00000000..18612f32
--- /dev/null
+++ b/src/lib/prisma/migrations/03_grading_result/migration.sql
@@ -0,0 +1,19 @@
+-- CreateTable
+CREATE TABLE "public"."gradingResult" (
+ "uuid" UUID NOT NULL,
+ "project_id" INTEGER NOT NULL,
+ "status" VARCHAR(255),
+ "result" VARCHAR(2000),
+ "publisher_id" VARCHAR(255) NOT NULL,
+ "lambda_request_id" VARCHAR(255),
+ "created" TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP,
+ "updated" TIMESTAMP(6),
+
+ CONSTRAINT "gradingResult_pkey" PRIMARY KEY ("uuid")
+);
+
+-- CreateIndex
+CREATE INDEX "idx_grading_result_project_id" ON "public"."gradingResult"("project_id");
+
+-- AddForeignKey
+ALTER TABLE "public"."gradingResult" ADD CONSTRAINT "fk_grading_result_project_id" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
diff --git a/src/lib/prisma/schema.prisma b/src/lib/prisma/schema.prisma
index 46eb7309..a785ba4f 100644
--- a/src/lib/prisma/schema.prisma
+++ b/src/lib/prisma/schema.prisma
@@ -4,7 +4,7 @@ generator client {
datasource db {
provider = "postgresql"
- url = env("DATABASE_URL")
+ url = env("DATABASE_URL")
}
model build {
@@ -62,21 +62,22 @@ model job {
}
model project {
- id Int @id @default(autoincrement())
- status String? @db.VarChar(255)
- result String? @db.VarChar(255)
- error String? @db.VarChar(2083)
- url String? @db.VarChar(1024)
- user_id String? @db.VarChar(255) // ISSUE #77: remove this?
- group_id String? @db.VarChar(255) // ISSUE #77: remove this?
- app_id String? @db.VarChar(255)
- project_name String? @db.VarChar(255)
- language_code String? @db.VarChar(255)
- publishing_key String? @db.VarChar(1024) // ISSUE #77: remove this?
- created DateTime? @default(now()) @db.Timestamp(6)
- updated DateTime? @updatedAt @db.Timestamp(6)
+ id Int @id @default(autoincrement())
+ status String? @db.VarChar(255)
+ result String? @db.VarChar(255)
+ error String? @db.VarChar(2083)
+ url String? @db.VarChar(1024)
+ user_id String? @db.VarChar(255) // ISSUE #77: remove this?
+ group_id String? @db.VarChar(255) // ISSUE #77: remove this?
+ app_id String? @db.VarChar(255)
+ project_name String? @db.VarChar(255)
+ language_code String? @db.VarChar(255)
+ publishing_key String? @db.VarChar(1024) // ISSUE #77: remove this?
+ created DateTime? @default(now()) @db.Timestamp(6)
+ updated DateTime? @updatedAt @db.Timestamp(6)
client_id Int?
- client client? @relation(fields: [client_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_project_client_id")
+ gradingResult gradingResult[]
+ client client? @relation(fields: [client_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_project_client_id")
@@index([client_id], map: "idx_project_client_id")
}
@@ -112,3 +113,17 @@ model appVersion {
created DateTime @default(now()) @db.Timestamp(6)
updated DateTime? @updatedAt @db.Timestamp(6)
}
+
+model gradingResult {
+ uuid String @id @default(uuid()) @db.Uuid
+ project_id Int
+ status String? @db.VarChar(255)
+ result String? @db.VarChar(2000)
+ publisher_id String @db.VarChar(255)
+ lambda_request_id String? @db.VarChar(255)
+ created DateTime? @default(now()) @db.Timestamp(6)
+ updated DateTime? @updatedAt @db.Timestamp(6)
+ project project @relation(fields: [project_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_grading_result_project_id")
+
+ @@index([project_id], map: "idx_grading_result_project_id")
+}
diff --git a/src/lib/server/aws/lambda.ts b/src/lib/server/aws/lambda.ts
new file mode 100644
index 00000000..fe59eca4
--- /dev/null
+++ b/src/lib/server/aws/lambda.ts
@@ -0,0 +1,65 @@
+import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';
+import { SpanStatusCode, trace } from '@opentelemetry/api';
+import { AWSVars } from './vars';
+
+const tracer = trace.getTracer('Lambda');
+
+export class Lambda {
+ private client;
+
+ public constructor() {
+ this.client = new LambdaClient({ region: AWSVars.artifactsRegion() });
+ }
+
+ public async invokeJson
+ View project grading reports, inspect report status, open generated HTML and JSON report + files, or request a new grading report for a project. +
+ Grading Administration » +diff --git a/src/routes/(ui)/grading-admin/+page.server.ts b/src/routes/(ui)/grading-admin/+page.server.ts new file mode 100644 index 00000000..d815830d --- /dev/null +++ b/src/routes/(ui)/grading-admin/+page.server.ts @@ -0,0 +1,49 @@ +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import type { Actions, PageServerLoad } from './$types'; +import { Grading } from '$lib/server/models/grading'; +import { prisma } from '$lib/server/prisma'; +import { tableSchema } from '$lib/valibot'; + +export const load = (async () => { + const gradingResults = await prisma.gradingResult.findMany({ + take: 20, + orderBy: { created: 'desc' }, + include: { project: true } + }); + return { + gradingResults: gradingResults.map((r) => Grading.response(r)), + count: await prisma.gradingResult.count(), + form: await superValidate( + { + page: { + page: 0, + size: 20 + } + }, + valibot(tableSchema) + ) + }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + page: async function ({ request }) { + const form = await superValidate(request, valibot(tableSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const gradingResults = await prisma.gradingResult.findMany({ + orderBy: form.data.sort ? { [form.data.sort.field]: form.data.sort.direction } : undefined, + skip: form.data.page.page * form.data.page.size, + take: form.data.page.size, + include: { project: true } + }); + + return { + form, + ok: true, + query: { + data: gradingResults + } + }; + } +}; diff --git a/src/routes/(ui)/grading-admin/+page.svelte b/src/routes/(ui)/grading-admin/+page.svelte new file mode 100644 index 00000000..fd6f6343 --- /dev/null +++ b/src/routes/(ui)/grading-admin/+page.svelte @@ -0,0 +1,138 @@ + + +
+ Showing + {$form.page.page * $form.page.size + 1}-{Math.min( + ($form.page.page + 1) * $form.page.size, + data.count + )} + + of + {data.count} + items +
+| ID | +{data.gradingResult.uuid} | +
|---|---|
| Project ID | ++ + {data.gradingResult.project_id} + + | +
| Project Name | +{data.rawGradingResult.project.project_name} | +
| Publisher | +{data.gradingResult.publisher_id} | +
| Status | +{data.gradingResult.status} | +
| Result | +{data.gradingResult.result} | +
| Reports | ++ {#if data.gradingResult.reports.html} + HTML + + {/if} + {#if data.gradingResult.reports.json} + JSON + {/if} + | +
| Lambda Request ID | +{data.rawGradingResult.lambda_request_id} | +
| Created | +{getTimeDateString(data.gradingResult.created)} | +
| Updated | +{getTimeDateString(data.gradingResult.updated)} | +
| ID | +Status | +Result | +Publisher | +Created | ++ |
|---|---|---|---|---|---|
| {gradingResult.uuid} | +{gradingResult.status} | +{gradingResult.result} | +{gradingResult.publisher_id} | +{getTimeDateString(gradingResult.created)} | +
+ {#if gradingResult.reports.html}
+
+ |
+
| No grading reports | +|||||