From 0787f19dce763850a29e9a1ac268dc711c0606e8 Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Thu, 4 Apr 2024 19:27:54 -0400 Subject: [PATCH 01/16] Grader service updates WIP, first draft mostly done --- .../src/entities/grader/grader.controller.ts | 10 +-- .../src/entities/grader/grader.service.ts | 71 +++++++++++++------ .../entities/submission/submission.service.ts | 2 +- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/devU-api/src/entities/grader/grader.controller.ts b/devU-api/src/entities/grader/grader.controller.ts index bd766ca0..1a2ced3d 100644 --- a/devU-api/src/entities/grader/grader.controller.ts +++ b/devU-api/src/entities/grader/grader.controller.ts @@ -2,17 +2,17 @@ import { Request, Response, NextFunction } from 'express' import GraderService from './grader.service' -import { GenericResponse, NotFound } from '../../utils/apiResponse.utils' +import { GenericResponse } from '../../utils/apiResponse.utils' //, NotFound -import { serialize } from '../grader/grader.serializer' +//import { serialize } from '../grader/grader.serializer' export async function grade(req: Request, res: Response, next: NextFunction) { try { const submissionId = parseInt(req.params.id) - const grade = await GraderService.grade(submissionId) - if (!grade || grade.length === 0) return res.status(404).json(NotFound) + const response = await GraderService.grade(submissionId) //grade + //if (!grade || grade.length === 0) return res.status(404).json(NotFound) - const response = serialize(grade) + //const response = serialize(grade) res.status(200).json(response) } catch (err) { diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index bb248f49..2333ff62 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -5,12 +5,16 @@ import nonContainerAutograderService from '../nonContainerAutoGrader/nonContaine import containerAutograderService from '../containerAutoGrader/containerAutoGrader.service' import assignmentProblemService from '../assignmentProblem/assignmentProblem.service' import assignmentScoreService from '../assignmentScore/assignmentScore.service' +import courseService from '../course/course.service' +import { addJob, openDirectory, uploadFile } from '../../tango/tango.service' import { SubmissionScore, SubmissionProblemScore, ContainerAutoGrader, AssignmentScore } from 'devu-shared-modules' import { checkAnswer } from '../nonContainerAutoGrader/nonContainerAutoGrader.grader' import { serialize as serializeNonContainer } from '../nonContainerAutoGrader/nonContainerAutoGrader.serializer' -import { serialize as serializeContainer } from '../containerAutoGrader/containerAutoGrader.serializer' import { serialize as serializeAssignmentScore } from '../assignmentScore/assignmentScore.serializer' +import { downloadFile } from '../../fileStorage' + +import crypto from 'crypto' export async function grade(submissionId: number) { const submission = await submissionService.retrieve(submissionId) @@ -20,10 +24,10 @@ export async function grade(submissionId: number) { const content = JSON.parse(submission.content) const form = content.form - const filepaths = content.filepaths //Using the field name that was written on the whiteboard for now + const filepaths: string[] = content.filepaths //Using the field name that was written on the whiteboard for now const nonContainerAutograders = await nonContainerAutograderService.listByAssignmentId(assignmentId) - const containerAutograders = await containerAutograderService.listByAssignmentId(assignmentId) + //const containerAutograders = await containerAutograderService.listByAssignmentId(assignmentId) const assignmentProblems = await assignmentProblemService.list(assignmentId) @@ -52,26 +56,49 @@ export async function grade(submissionId: number) { } //Run Container Autograders - //Mock functionality, this is not finalized!!!! - for (const filepath of filepaths) { - const containerGrader = containerAutograders.find(grader => grader.autogradingImage === filepath) //PLACEHOLDER, I'm just using autogradingImage temporarily to associate graders to files - - if (containerGrader) { - const gradeResults = await mockContainerCheckAnswer(filepath, serializeContainer(containerGrader)) - - for (const result of gradeResults) { - const problemScoreObj: SubmissionProblemScore = { - submissionId: submissionId, - assignmentProblemId: 1, //PLACEHOLDER, an assignmentProblem must exist in the db for this to work - score: result.score, - feedback: result.feedback, + try { + const {graderData, makefileData, autogradingImage, timeout} = await containerAutograderService.getGraderByAssignmentId(assignmentId) + const bucketName = await courseService.retrieve(submission.courseId).then((course) => { + return course ? ((course.name).toLowerCase().replace(/ /g, '-') + course.number + course.semester + course.id).toLowerCase() : 'submission' + }) + + const labName = bucketName + '-' + submission.assignmentId + const optionFiles = [] + const openResponse = await openDirectory(labName) + var response = null + if (openResponse) { + if (!(openResponse.files["Graderfile"]) || openResponse.files["Graderfile"] !== crypto.createHash('md5').update(graderData).digest('hex')) { + const graderFile = new File([new Blob([graderData])], "Graderfile") + await uploadFile(labName, graderFile, "Graderfile") + } + if (!(openResponse.files["Makefile"]) || openResponse.files["Makefile"] !== crypto.createHash('md5').update(makefileData).digest('hex')) { + const makefile = new File([new Blob([makefileData])], "Makefile") + await uploadFile(labName, makefile, "Makefile") + } + for (const filepath of filepaths){ + const buffer = await downloadFile(bucketName, filepath) + const file = new File([new Blob([buffer])], filepath) + if (await uploadFile(labName, file, filepath)) { + optionFiles.push({localFile: filepath, destFile: filepath}) } - allScores.push(await submissionProblemScoreService.create(problemScoreObj)) - score += result.score - feedback += result.feedback + '\n' } + + const jobOptions = { + image: autogradingImage, + files: [{localFile: "Graderfile", destFile: "Graderfile"}, + {localFile: "Makefile", destFile: "Makefile"},] + .concat(optionFiles), + jobName: labName, + output_file: labName, + timeout: timeout, + callback_url: "" + } + response = await addJob(labName, jobOptions) } - } + } catch (e) { + throw e + } + //remember, immediate callback is made when job has been added to queue, not sure how we're handling the rest of it yet though lmao //Grading is finished. Create SubmissionScore and AssignmentScore and save to db. const scoreObj: SubmissionScore = { @@ -87,7 +114,7 @@ export async function grade(submissionId: number) { const assignmentScore = serializeAssignmentScore(assignmentScoreModel) assignmentScore.score = score assignmentScoreService.update(assignmentScore) - + } else { //Otherwise make a new one const assignmentScore: AssignmentScore = { assignmentId: submission.assignmentId, @@ -97,7 +124,7 @@ export async function grade(submissionId: number) { assignmentScoreService.create(assignmentScore) } - return allScores + return response } //Temporary mock function, delete when the container autograder grading function is written diff --git a/devU-api/src/entities/submission/submission.service.ts b/devU-api/src/entities/submission/submission.service.ts index 2a54dd4a..bb205b6d 100644 --- a/devU-api/src/entities/submission/submission.service.ts +++ b/devU-api/src/entities/submission/submission.service.ts @@ -32,7 +32,7 @@ export async function create(submission: Submission, file?: Express.Multer.File filename: filename, } const content = JSON.parse(submission.content) - content.filepaths.push(filename) + content.filepaths.push(fileModel.filename) submission.content = JSON.stringify(content) await fileConn().save(fileModel) From 549497d1baecb389b3142cc6fb93ca4c3c7772ba Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Fri, 5 Apr 2024 02:43:59 -0400 Subject: [PATCH 02/16] misc fixes for grader service updates, first draft still not functional --- devU-api/package-lock.json | 64 +++++++++++++++++++ devU-api/package.json | 4 +- .../containerAutoGrader.model.ts | 14 ++-- .../containerAutoGrader.router.ts | 2 +- .../containerAutoGrader.service.ts | 1 + .../src/entities/grader/grader.controller.ts | 2 +- .../src/entities/grader/grader.service.ts | 2 +- .../entities/submission/submission.model.ts | 4 +- .../entities/submission/submission.router.ts | 2 +- .../entities/submission/submission.service.ts | 2 +- devU-api/src/fileStorage.ts | 2 +- devU-api/src/tango/tango.service.ts | 3 +- 12 files changed, 86 insertions(+), 16 deletions(-) diff --git a/devU-api/package-lock.json b/devU-api/package-lock.json index 927af045..b4be152a 100644 --- a/devU-api/package-lock.json +++ b/devU-api/package-lock.json @@ -21,6 +21,7 @@ "minio": "^7.0.18", "morgan": "^1.10.0", "multer": "^1.4.2", + "node-fetch": "^2.7.0", "passport": "^0.6.0", "passport-saml": "^3.2.2", "pg": "^8.4.0", @@ -43,6 +44,7 @@ "@types/morgan": "^1.9.2", "@types/multer": "^1.4.7", "@types/node": "^15.12.2", + "@types/node-fetch": "^2.6.11", "@types/passport": "^1.0.6", "@types/passport-strategy": "^0.2.35", "@types/swagger-jsdoc": "^6.0.0", @@ -2149,6 +2151,30 @@ "integrity": "sha512-dvMUE/m2LbXPwlvVuzCyslTEtQ2ZwuuFClDrOQ6mp2CenCg971719PTILZ4I6bTP27xfFFc+o7x2TkLuun/MPw==", "dev": true }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -9271,6 +9297,44 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/devU-api/package.json b/devU-api/package.json index 647ccabe..87acfc77 100644 --- a/devU-api/package.json +++ b/devU-api/package.json @@ -14,7 +14,7 @@ "pre-commit": "lint-staged", "generate-config": "docker run --pull always -v $(pwd)/config:/config --user $(id -u):$(id -g) --rm ubautograding/devtools /generateConfig.sh config/default.yml", "populate-db": "ts-node-dev ./scripts/populate-db.ts", - "tango": "ts-node-dev ./src/tango/tests/tango.endpoint.test.ts" + "tango": "ts-node-dev ./src/tango/tests/tango.endpoint.test.ts" }, "lint-staged": { "./**/*.{js,ts,json,md}": [ @@ -40,6 +40,7 @@ "@types/morgan": "^1.9.2", "@types/multer": "^1.4.7", "@types/node": "^15.12.2", + "@types/node-fetch": "^2.6.11", "@types/passport": "^1.0.6", "@types/passport-strategy": "^0.2.35", "@types/swagger-jsdoc": "^6.0.0", @@ -69,6 +70,7 @@ "minio": "^7.0.18", "morgan": "^1.10.0", "multer": "^1.4.2", + "node-fetch": "^2.7.0", "passport": "^0.6.0", "passport-saml": "^3.2.2", "pg": "^8.4.0", diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts index bf66321b..0f012bf3 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts @@ -22,21 +22,21 @@ export default class ContainerAutoGraderModel { * schemas: * ContainerAutoGrader: * type: object - * required: [assignmentId, graderFile, autogradingImage, timeout] + * required: [assignmentId, autogradingImage, timeout, graderFile, makefileFile] * properties: * assignmentId: * type: integer - * graderFile: - * type: string - * description: Filename of already uploaded grader file - * makeFileFile: - * type: string - * description: Filename of already uploaded makefile * autogradingImage: * type: string * timeout: * type: integer * description: Must be a positive integer + * graderFile: + * type: string + * format: binary + * makefileFile: + * type: string + * format: binary */ @PrimaryGeneratedColumn() diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts index 398bb016..ff6b37f1 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts @@ -53,7 +53,7 @@ Router.get('/:id', asInt(), ContainerAutoGraderController.detail); * description: OK * requestBody: * content: - * application/x-www-form-urlencoded: + * multipart/form-data: * schema: * $ref: '#/components/schemas/ContainerAutoGrader' */ diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts index 782e3f0d..271accbe 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts @@ -42,6 +42,7 @@ export async function create(containerAutoGrader: ContainerAutoGrader, graderInp containerAutoGrader.graderFile = filename if (makefileInputFile) { + const bucket: string = 'makefiles' const makefileFilename: string = generateFilename(makefileInputFile.originalname, userId) await filesUpload(bucket, makefileInputFile, containerAutoGrader, makefileFilename) containerAutoGrader.makefileFile = makefileFilename diff --git a/devU-api/src/entities/grader/grader.controller.ts b/devU-api/src/entities/grader/grader.controller.ts index 1a2ced3d..ab2a622c 100644 --- a/devU-api/src/entities/grader/grader.controller.ts +++ b/devU-api/src/entities/grader/grader.controller.ts @@ -14,7 +14,7 @@ export async function grade(req: Request, res: Response, next: NextFunction) { //const response = serialize(grade) - res.status(200).json(response) + res.status(200).json({message: response?.jobId + ", " + response?.statusId + ", " + response?.statusMsg}) } catch (err) { res.status(400).json(new GenericResponse(err.message)) } diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index 2333ff62..6f16f789 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -96,7 +96,7 @@ export async function grade(submissionId: number) { response = await addJob(labName, jobOptions) } } catch (e) { - throw e + console.log(e) } //remember, immediate callback is made when job has been added to queue, not sure how we're handling the rest of it yet though lmao diff --git a/devU-api/src/entities/submission/submission.model.ts b/devU-api/src/entities/submission/submission.model.ts index fdf2fda0..8dae3b14 100644 --- a/devU-api/src/entities/submission/submission.model.ts +++ b/devU-api/src/entities/submission/submission.model.ts @@ -24,7 +24,6 @@ export default class SubmissionModel { * schemas: * Submission: * type: object - * required: [courseId, assignmentId, userId, content, type, submitterIp, submittedBy] * properties: * courseId: * type: integer @@ -38,6 +37,9 @@ export default class SubmissionModel { * type: string * submittedBy: * type: integer + * files: + * type: string + * format: binary */ @PrimaryGeneratedColumn() id: number diff --git a/devU-api/src/entities/submission/submission.router.ts b/devU-api/src/entities/submission/submission.router.ts index f5966369..8a899f4e 100644 --- a/devU-api/src/entities/submission/submission.router.ts +++ b/devU-api/src/entities/submission/submission.router.ts @@ -68,7 +68,7 @@ Router.get('/:id', asInt(), SubmissionController.detail) * description: OK * requestBody: * content: - * application/x-www-form-urlencoded: + * multipart/form-data: * schema: * $ref: '#/components/schemas/Submission' */ diff --git a/devU-api/src/entities/submission/submission.service.ts b/devU-api/src/entities/submission/submission.service.ts index bb205b6d..f8b70400 100644 --- a/devU-api/src/entities/submission/submission.service.ts +++ b/devU-api/src/entities/submission/submission.service.ts @@ -32,7 +32,7 @@ export async function create(submission: Submission, file?: Express.Multer.File filename: filename, } const content = JSON.parse(submission.content) - content.filepaths.push(fileModel.filename) + content.filepaths = [fileModel.filename] submission.content = JSON.stringify(content) await fileConn().save(fileModel) diff --git a/devU-api/src/fileStorage.ts b/devU-api/src/fileStorage.ts index 00d9bd19..87a8fa79 100644 --- a/devU-api/src/fileStorage.ts +++ b/devU-api/src/fileStorage.ts @@ -62,11 +62,11 @@ export async function downloadFile(bucketName: string, filename: string): Promis return new Promise((resolve, reject) => { const fileData: Buffer[] = [] + console.log(bucketName, filename) minioClient.getObject(bucketName, filename, (err, dataStream) => { if (err) { reject(new Error('File failed to download from MinIO because'+err.message)) } - dataStream.on('data', (chunk:any) => { fileData.push(chunk) }) diff --git a/devU-api/src/tango/tango.service.ts b/devU-api/src/tango/tango.service.ts index 59c335fe..e4075eea 100644 --- a/devU-api/src/tango/tango.service.ts +++ b/devU-api/src/tango/tango.service.ts @@ -1,4 +1,5 @@ import './tango.types' +import fetch from 'node-fetch' const tangoHost = `http://${(process.env.TANGO_KEY ?? 'localhost:3000')}` const tangoKey = process.env.TANGO_KEY ?? 'test' @@ -25,7 +26,7 @@ export async function uploadFile(courselab: string, file: File, fileName: string const url = `${tangoHost}/upload/${tangoKey}/${courselab}/` const formData = new FormData() formData.append('file', file) - const response = await fetch(url, { method: 'POST', body: formData, headers: { 'filename': fileName } }) + const response = await fetch(url, { method: 'POST', body: formData as any, headers: { 'filename': fileName } }) return response.ok ? await response.json() : null } From 9d8a60244757190044a9aeb7b85f2a3788a55c31 Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Fri, 5 Apr 2024 17:42:43 -0400 Subject: [PATCH 03/16] first draft of grader.service container autograding functional --- devU-api/src/entities/grader/grader.service.ts | 17 +++++++---------- devU-api/src/fileStorage.ts | 5 ++--- devU-api/src/tango/tango.service.ts | 8 +++----- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index 6f16f789..4336fedd 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -12,7 +12,7 @@ import { SubmissionScore, SubmissionProblemScore, ContainerAutoGrader, Assignmen import { checkAnswer } from '../nonContainerAutoGrader/nonContainerAutoGrader.grader' import { serialize as serializeNonContainer } from '../nonContainerAutoGrader/nonContainerAutoGrader.serializer' import { serialize as serializeAssignmentScore } from '../assignmentScore/assignmentScore.serializer' -import { downloadFile } from '../../fileStorage' +import { downloadFile, initializeMinio } from '../../fileStorage' import crypto from 'crypto' @@ -59,30 +59,27 @@ export async function grade(submissionId: number) { try { const {graderData, makefileData, autogradingImage, timeout} = await containerAutograderService.getGraderByAssignmentId(assignmentId) const bucketName = await courseService.retrieve(submission.courseId).then((course) => { - return course ? ((course.name).toLowerCase().replace(/ /g, '-') + course.number + course.semester + course.id).toLowerCase() : 'submission' + return course ? (course.number + course.semester + course.id).toLowerCase() : 'submission' }) + initializeMinio(bucketName) + var response = null const labName = bucketName + '-' + submission.assignmentId const optionFiles = [] const openResponse = await openDirectory(labName) - var response = null if (openResponse) { if (!(openResponse.files["Graderfile"]) || openResponse.files["Graderfile"] !== crypto.createHash('md5').update(graderData).digest('hex')) { - const graderFile = new File([new Blob([graderData])], "Graderfile") - await uploadFile(labName, graderFile, "Graderfile") + await uploadFile(labName, graderData, "Graderfile") } if (!(openResponse.files["Makefile"]) || openResponse.files["Makefile"] !== crypto.createHash('md5').update(makefileData).digest('hex')) { - const makefile = new File([new Blob([makefileData])], "Makefile") - await uploadFile(labName, makefile, "Makefile") + await uploadFile(labName, makefileData, "Makefile") } for (const filepath of filepaths){ const buffer = await downloadFile(bucketName, filepath) - const file = new File([new Blob([buffer])], filepath) - if (await uploadFile(labName, file, filepath)) { + if (await uploadFile(labName, buffer, filepath)) { optionFiles.push({localFile: filepath, destFile: filepath}) } } - const jobOptions = { image: autogradingImage, files: [{localFile: "Graderfile", destFile: "Graderfile"}, diff --git a/devU-api/src/fileStorage.ts b/devU-api/src/fileStorage.ts index 87a8fa79..91390e78 100644 --- a/devU-api/src/fileStorage.ts +++ b/devU-api/src/fileStorage.ts @@ -50,7 +50,7 @@ export async function uploadFile(bucketName: string, file: Express.Multer.File, return new Promise((resolve, reject) => { minioClient.putObject(bucketName, filename, file.buffer, (err, etag) => { if (err) { - reject(new Error('File failed to upload because'+err.message)) + reject(new Error('File failed to upload because '+err.message)) } else { resolve(etag.etag) } @@ -62,10 +62,9 @@ export async function downloadFile(bucketName: string, filename: string): Promis return new Promise((resolve, reject) => { const fileData: Buffer[] = [] - console.log(bucketName, filename) minioClient.getObject(bucketName, filename, (err, dataStream) => { if (err) { - reject(new Error('File failed to download from MinIO because'+err.message)) + reject(new Error('File failed to download from MinIO because '+err.message)) } dataStream.on('data', (chunk:any) => { fileData.push(chunk) diff --git a/devU-api/src/tango/tango.service.ts b/devU-api/src/tango/tango.service.ts index e4075eea..82001d14 100644 --- a/devU-api/src/tango/tango.service.ts +++ b/devU-api/src/tango/tango.service.ts @@ -1,7 +1,7 @@ import './tango.types' import fetch from 'node-fetch' -const tangoHost = `http://${(process.env.TANGO_KEY ?? 'localhost:3000')}` +const tangoHost = `http://tango:3000` const tangoKey = process.env.TANGO_KEY ?? 'test' // for more info https://docs.autolabproject.com/tango-rest/ @@ -22,11 +22,9 @@ export async function openDirectory(courselab: string): Promise { +export async function uploadFile(courselab: string, file: Buffer, fileName: string): Promise { const url = `${tangoHost}/upload/${tangoKey}/${courselab}/` - const formData = new FormData() - formData.append('file', file) - const response = await fetch(url, { method: 'POST', body: formData as any, headers: { 'filename': fileName } }) + const response = await fetch(url, { method: 'POST', body: file, headers: { 'filename': fileName } }) return response.ok ? await response.json() : null } From cef7d7893b8e8cdc52c2e8908ee8cf38e700f869 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 21 Apr 2024 00:20:26 -0400 Subject: [PATCH 04/16] renames some vars --- devU-api/src/tango/tango.service.ts | 54 +++++++++++++++++------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/devU-api/src/tango/tango.service.ts b/devU-api/src/tango/tango.service.ts index 59c335fe..cf720a53 100644 --- a/devU-api/src/tango/tango.service.ts +++ b/devU-api/src/tango/tango.service.ts @@ -1,56 +1,57 @@ import './tango.types' +import fetch from "node-fetch"; -const tangoHost = `http://${(process.env.TANGO_KEY ?? 'localhost:3000')}` +const tangoHost = `http://127.0.0.1:3000` const tangoKey = process.env.TANGO_KEY ?? 'test' // for more info https://docs.autolabproject.com/tango-rest/ /** - * Opens a directory for a given course and lab. - * @param courselab - The combination of the course name and the lab name. + * Opens a directory for a given course. + * @param course - The course name. */ -export async function openDirectory(courselab: string): Promise { - const url = `${tangoHost}/open/${tangoKey}/${courselab}/` +export async function createCourse(course: string): Promise { + const url = `${tangoHost}/open/${tangoKey}/${course}/` const response = await fetch(url, { method: 'GET' }) - return response.ok ? await response.json() : null + return response.ok ? await response.json() as OpenResponse : null } /** - * Uploads a file to the server for a given course and lab. - * @param courselab - The combination of the course name and the lab name. + * Uploads a file to the server for a given course. + * @param course - The course name. * @param fileName - The file name, used to identify the file when uploaded * @param file - The file to be uploaded. */ -export async function uploadFile(courselab: string, file: File, fileName: string): Promise { - const url = `${tangoHost}/upload/${tangoKey}/${courselab}/` +export async function uploadFile(course: string, file: File, fileName: string): Promise { + const url = `${tangoHost}/upload/${tangoKey}/${course}/` const formData = new FormData() formData.append('file', file) const response = await fetch(url, { method: 'POST', body: formData, headers: { 'filename': fileName } }) - return response.ok ? await response.json() : null + return response.ok ? await response.json() as UploadResponse : null } /** - * Adds a job to the server for a given course and lab. - * @param courselab - The combination of the course name and the lab name. + * Adds a job to the server for a given course. + * @param course - The course name. * @param job - The job request object. */ -export async function addJob(courselab: string, job: AddJobRequest): Promise { - const url = `${tangoHost}/addJob/${tangoKey}/${courselab}/` +export async function addJob(course: string, job: AddJobRequest): Promise { + const url = `${tangoHost}/addJob/${tangoKey}/${course}/` const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(job), }) - return response.ok ? await response.json() : null + return response.ok ? await response.json() as AddJobResponse : null } /** * Polls the server for the status of a job. - * @param courselab - The combination of the course name and the lab name. + * @param course - The course name. * @param outputFile - The name of the output file. */ -export async function pollJob(courselab: string, outputFile: string): Promise { - const url = `${tangoHost}/poll/${tangoKey}/${courselab}/${outputFile}/` +export async function pollJob(course: string, outputFile: string): Promise { + const url = `${tangoHost}/poll/${tangoKey}/${course}/${outputFile}/` const response = await fetch(url, { method: 'GET' }) const data = await response.json() return response.ok ? @@ -58,13 +59,22 @@ export async function pollJob(courselab: string, outputFile: string): Promise { + const url = `${tangoHost}/` + const response = await fetch(url, { method: 'GET' }) + return response.ok +} + /** * Retrieves information about the Tango service. */ export async function getInfo(): Promise { const url = `${tangoHost}/info/${tangoKey}/` const response = await fetch(url, { method: 'GET' }) - return response.ok ? await response.json() : null + return response.ok ? await response.json() as InfoResponse: null } /** @@ -74,7 +84,7 @@ export async function getInfo(): Promise { export async function getPoolInfo(image: string): Promise { const url = `${tangoHost}/pool/${tangoKey}/${image}/` const response = await fetch(url, { method: 'GET' }) - return response.ok ? await response.json() : null + return response.ok ? await response.json() as Object: null } /** @@ -90,7 +100,7 @@ export async function preallocateInstances(image: string, num: number, request: headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(request), }) - return response.ok ? await response.json() : null + return response.ok ? await response.json() as PreallocResponse: null } /** From f70141097bedaded9075eb0e914e2f87c6480321 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 21 Apr 2024 00:21:31 -0400 Subject: [PATCH 05/16] added test files --- devU-api/src/tango/tests/test_files/Makefile | 4 + .../src/tango/tests/test_files/autograde.tar | Bin 0 -> 10240 bytes .../tango/tests/test_files/create_grader.sh | 3 + devU-api/src/tango/tests/test_files/grader.py | 98 ++++++++++++++++++ devU-api/src/tango/tests/test_files/grader.sh | 3 + devU-api/src/tango/tests/test_files/handin.py | 10 ++ .../tests/test_files/handin_incorrect1.py | 8 ++ .../tests/test_files/handin_incorrect2.py | 8 ++ .../tests/test_files/handin_incorrect3.py | 8 ++ .../tests/test_files/handin_incorrect4.py | 8 ++ 10 files changed, 150 insertions(+) create mode 100644 devU-api/src/tango/tests/test_files/Makefile create mode 100644 devU-api/src/tango/tests/test_files/autograde.tar create mode 100644 devU-api/src/tango/tests/test_files/create_grader.sh create mode 100644 devU-api/src/tango/tests/test_files/grader.py create mode 100644 devU-api/src/tango/tests/test_files/grader.sh create mode 100644 devU-api/src/tango/tests/test_files/handin.py create mode 100644 devU-api/src/tango/tests/test_files/handin_incorrect1.py create mode 100644 devU-api/src/tango/tests/test_files/handin_incorrect2.py create mode 100644 devU-api/src/tango/tests/test_files/handin_incorrect3.py create mode 100644 devU-api/src/tango/tests/test_files/handin_incorrect4.py diff --git a/devU-api/src/tango/tests/test_files/Makefile b/devU-api/src/tango/tests/test_files/Makefile new file mode 100644 index 00000000..629e3e13 --- /dev/null +++ b/devU-api/src/tango/tests/test_files/Makefile @@ -0,0 +1,4 @@ +all: + tar -xf autograde.tar + chmod +x grader.sh + ./grader.sh \ No newline at end of file diff --git a/devU-api/src/tango/tests/test_files/autograde.tar b/devU-api/src/tango/tests/test_files/autograde.tar new file mode 100644 index 0000000000000000000000000000000000000000..295b59c02881804510015d27f6857219bfe3f702 GIT binary patch literal 10240 zcmeHK-EZ135a&66#g#n_R7UMMAuSS*)3o>QsjW~b1~P$@Ii><3{`Z|@=L4wvF%5N- z><2hIzPn$3_rWoj@s!Jcwr!nn5nNFeweX8ZA!&Z=M?n}yEgB4nPgxL!L5ol_h=LZ7 z^FgXDuX7bkfR`=7;pcgXomFc#&2$z+3Y<}@Oi9~Cq7m@S9zuUr$eB|3?A1q z^BZ#G4kPus-?&d3=d5w17PS!%L1k&46rj@S^avPUVFnAD2#pjDAu>`lgc~DizZNvr zW~~y{j;$I+J!%yx(U(YhixB+`E3kI}hd*&~gagMzU|@;~D~fl<&_NI%13&kVYoU5_m8#O*4R;*VO zE8sCds%sWhAC=(G3tQ>V` zsf0viYXo0F?E&9p{83?@VU4Vp3ZGVbCNBL%@l{@ypS?ALmk8)IU6^W1W>Bh*j(r+( zba9({yVG^{0GA_7U45#TsnJ>cMYp@A-+dQ7s^J0haQ6LtE;O zt2Xc<;q_g3Uk|Ttjei{9y03mnfSC($8DQ*u@p9R@JLqjA>}{l1_d8t>$7hiRkMF^2~3=KQ|NVArI&Od~rr(ddPoa{wIVDPUio> zck}-lLsFUkba6FF#MLCu7x;^}Rf|+Gut{;&KZO+nCpZQi1C9a5fMdWh;23ZWI0hU8 ijseGjW56-s7;p?Y1{?#90mp!2z%k$$a18uS27Uu;5ElIa literal 0 HcmV?d00001 diff --git a/devU-api/src/tango/tests/test_files/create_grader.sh b/devU-api/src/tango/tests/test_files/create_grader.sh new file mode 100644 index 00000000..9becea49 --- /dev/null +++ b/devU-api/src/tango/tests/test_files/create_grader.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +tar -cf autograde.tar grader.py grader.sh \ No newline at end of file diff --git a/devU-api/src/tango/tests/test_files/grader.py b/devU-api/src/tango/tests/test_files/grader.py new file mode 100644 index 00000000..d425e66c --- /dev/null +++ b/devU-api/src/tango/tests/test_files/grader.py @@ -0,0 +1,98 @@ +import json + + +def finished_grading(): + print("\nGrader finished") + print(json.dumps({"scores": scores})) + exit(0) + + +print("Grader starting") + +scores = { + "q1": 0, + "q2": 0, + "q3": 0, +} + +negative_tests = { + -9999987654321: 1, + -99: 1, + -32: 1, + -31: 1, + -30: 1, + -29: 1, + -10: 1, + -3: 1, + -2: 1, + -1: 1 +} + +positive_odd_tests = { + 1: 3, + 3: 5, + 5: 7, + 7: 9, + 9: 11, + 11: 13, + 13: 15, + 15: 17, + 101: 103, + 1005: 1007, +} + +positive_even_tests = { + 2: 3, + 4: 5, + 6: 7, + 8: 9, + 10: 11, + 100: 101, + 1000: 1001, + 12345678: 12345679, + 2222: 2223, + 3336: 3337, +} + +try: + from handin import get_next_positive_odd_number +except ImportError: + print("Unable to import get_next_positive_odd_number from handin.py") + finished_grading() + +print("\nTesting negative numbers") +for input_number, expected_result in negative_tests.items(): + result = get_next_positive_odd_number(input_number) + if result == expected_result: + scores["q1"] += 1 + print(f"get_next_positive_odd_number({input_number}) returned {result}, and that's correct! (+1)") + else: + print( + f"get_next_positive_odd_number({input_number}) incorrectly returned {result}, expected {expected_result}. (+0)") + +print("\nTesting positive odd numbers") +for input_number, expected_result in positive_odd_tests.items(): + result = get_next_positive_odd_number(input_number) + if result == expected_result: + scores["q2"] += 1 + print(f"get_next_positive_odd_number({input_number}) returned {result}, and that's correct! (+1)") + else: + print( + f"get_next_positive_odd_number({input_number}) incorrectly returned {result}, expected {expected_result}. (+0)") + +print("\nTesting positive even numbers") +for input_number, expected_result in positive_even_tests.items(): + result = get_next_positive_odd_number(input_number) + if result == expected_result: + scores["q3"] += 1 + print(f"get_next_positive_odd_number({input_number}) returned {result}, and that's correct! (+1)") + else: + print( + f"get_next_positive_odd_number({input_number}) incorrectly returned {result}, expected {expected_result}. (+0)") + +if all([scores["q1"] == 10, scores["q2"] == 10, scores["q3"] == 10]): + print("\nGreat job!") +else: + print("\nTry again.") + +finished_grading() diff --git a/devU-api/src/tango/tests/test_files/grader.sh b/devU-api/src/tango/tests/test_files/grader.sh new file mode 100644 index 00000000..c1aa59e8 --- /dev/null +++ b/devU-api/src/tango/tests/test_files/grader.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +python3 grader.py \ No newline at end of file diff --git a/devU-api/src/tango/tests/test_files/handin.py b/devU-api/src/tango/tests/test_files/handin.py new file mode 100644 index 00000000..978677d4 --- /dev/null +++ b/devU-api/src/tango/tests/test_files/handin.py @@ -0,0 +1,10 @@ +# This is an example of a correct solution. +# It can be helpful to test your grader with the correct solution in the same directory where the student's handin +# will end up. You don't need to include the correct solution in the grader archive. + +def get_next_positive_odd_number(number: int) -> int: + # Given `number`, return the next positive odd number. + # If `number` is negative, return 1. + # If `number` is positive and odd, return `number` + 2. + # If `number` is positive and even, return `number` + 1. + return max(1, number + (number % 2) + 1) diff --git a/devU-api/src/tango/tests/test_files/handin_incorrect1.py b/devU-api/src/tango/tests/test_files/handin_incorrect1.py new file mode 100644 index 00000000..f44068cb --- /dev/null +++ b/devU-api/src/tango/tests/test_files/handin_incorrect1.py @@ -0,0 +1,8 @@ +# This is an example of an incorrect solution. + +def get_next_positive_odd_number(number: int) -> int: + # Given `number`, return the next positive odd number. + # If `number` is negative, return 1. + # If `number` is positive and odd, return `number` + 2. + # If `number` is positive and even, return `number` + 1. + return max(1, number + (number % 2)) diff --git a/devU-api/src/tango/tests/test_files/handin_incorrect2.py b/devU-api/src/tango/tests/test_files/handin_incorrect2.py new file mode 100644 index 00000000..dabbdbdd --- /dev/null +++ b/devU-api/src/tango/tests/test_files/handin_incorrect2.py @@ -0,0 +1,8 @@ +# This is an example of an incorrect solution. + +def get_next_positive_odd_number(number: int) -> int: + # Given `number`, return the next positive odd number. + # If `number` is negative, return 1. + # If `number` is positive and odd, return `number` + 2. + # If `number` is positive and even, return `number` + 1. + return max(1, number + (number % 3) + 1) diff --git a/devU-api/src/tango/tests/test_files/handin_incorrect3.py b/devU-api/src/tango/tests/test_files/handin_incorrect3.py new file mode 100644 index 00000000..995dd7c2 --- /dev/null +++ b/devU-api/src/tango/tests/test_files/handin_incorrect3.py @@ -0,0 +1,8 @@ +# This is an example of an incorrect solution. + +def get_next_positive_odd_number(number: int) -> int: + # Given `number`, return the next positive odd number. + # If `number` is negative, return 1. + # If `number` is positive and odd, return `number` + 2. + # If `number` is positive and even, return `number` + 1. + return min(1, number + (number % 3) + 1) diff --git a/devU-api/src/tango/tests/test_files/handin_incorrect4.py b/devU-api/src/tango/tests/test_files/handin_incorrect4.py new file mode 100644 index 00000000..9ecae225 --- /dev/null +++ b/devU-api/src/tango/tests/test_files/handin_incorrect4.py @@ -0,0 +1,8 @@ +# This is an example of an incorrect solution because the function name is incorrect. + +def incorrect_function_name(number: int) -> int: + # Given `number`, return the next positive odd number. + # If `number` is negative, return 1. + # If `number` is positive and odd, return `number` + 2. + # If `number` is positive and even, return `number` + 1. + return max(1, number + (number % 2) + 1) From 041feef60ac23cb8f930528b6c5d16b0d6525c5d Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 21 Apr 2024 00:24:21 -0400 Subject: [PATCH 06/16] added env vars needed for tango --- docker-compose.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index feaf0af2..f1816242 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,11 +91,7 @@ services: environment: - DOCKER_REDIS_HOSTNAME=redis - RESTFUL_KEY=devutangokey -# - DOCKER_DEPLOYMENT - # Path to volumes within the Tango container. Does not need to be modified. -# - DOCKER_VOLUME_PATH - # Modify the below to be the path to volumes on your host machine -# - DOCKER_TANGO_HOST_VOLUME_PATH + - DOCKER_TANGO_HOST_VOLUME_PATH= #TODO add the absolute path to tango_files here before running depends_on: - redis @@ -104,7 +100,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - ./logs/tango/:/var/log/tango/ - ./logs/tangonginx:/var/log/nginx -# - ./Tango/volumes:/opt/TangoService/Tango/volumes + - ./tango_files/volumes:/opt/TangoService/Tango/volumes # certbot: # container_name: certbot From 15442c6dda47cf2381fd1242cb60449de6266bd9 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 21 Apr 2024 00:26:56 -0400 Subject: [PATCH 07/16] added tango status --- devU-api/src/status/status.controller.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/devU-api/src/status/status.controller.ts b/devU-api/src/status/status.controller.ts index e3a1c3c8..1c1bff4a 100644 --- a/devU-api/src/status/status.controller.ts +++ b/devU-api/src/status/status.controller.ts @@ -1,5 +1,16 @@ import { Request, Response, NextFunction } from 'express' +import { getInfo, tangoHelloWorld } from '../tango/tango.service' export async function get(req: Request, res: Response, next: NextFunction) { - res.status(200).send() + let tango = false + let tangoStatus; + try { + tango = await tangoHelloWorld() + tangoStatus = await getInfo() + console.log(tangoStatus) + } catch (e) { + console.log('failed to start tango') + console.log(e) + } + res.status(200).send(`Api is online.\n Tango is ${tango ? `Online` : 'Offline'}`) } From 951da07e2e676e8742316d575f3843614edcf68d Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 21 Apr 2024 00:59:36 -0400 Subject: [PATCH 08/16] added node-fetch --- devU-api/package-lock.json | 39 ++++++++++++++++++++++++++++++++++++++ devU-api/package.json | 1 + 2 files changed, 40 insertions(+) diff --git a/devU-api/package-lock.json b/devU-api/package-lock.json index 927af045..8d145fe4 100644 --- a/devU-api/package-lock.json +++ b/devU-api/package-lock.json @@ -21,6 +21,7 @@ "minio": "^7.0.18", "morgan": "^1.10.0", "multer": "^1.4.2", + "node-fetch": "^2.7.0", "passport": "^0.6.0", "passport-saml": "^3.2.2", "pg": "^8.4.0", @@ -9271,6 +9272,44 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/devU-api/package.json b/devU-api/package.json index 6613c68a..1064985a 100644 --- a/devU-api/package.json +++ b/devU-api/package.json @@ -61,6 +61,7 @@ "body-parser": "^1.19.0", "colors": "^1.4.0", "config": "^3.3.6", + "node-fetch": "^2.7.0", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "devu-shared-modules": "./devu-shared-modules", From 48f5f4bef2de3486337e94e434f2b775a449f926 Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Thu, 25 Apr 2024 15:27:41 -0400 Subject: [PATCH 09/16] WIP for debugging --- .../containerAutoGrader.validator.ts | 2 +- .../src/entities/grader/grader.controller.ts | 14 +++++++++++++- devU-api/src/entities/grader/grader.router.ts | 2 ++ .../src/entities/grader/grader.service.ts | 19 ++++++++++++++----- devU-api/src/tango/tango.service.ts | 8 +++----- docker-compose.yml | 3 ++- 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.validator.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.validator.ts index 11740fb3..48f381f7 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.validator.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.validator.ts @@ -5,7 +5,7 @@ import validate from '../../middleware/validator/generic.validator' const assignmentId = check('assignmentId').isNumeric() -const graderFile = check('graderFile').custom(({req}) => { +const graderFile = check('graderFile').optional({ nullable: true }).custom(({req}) => { const file = req?.files['grader'] if (file !== null) { if (file.size <= 0) { diff --git a/devU-api/src/entities/grader/grader.controller.ts b/devU-api/src/entities/grader/grader.controller.ts index ab2a622c..1d08e5b7 100644 --- a/devU-api/src/entities/grader/grader.controller.ts +++ b/devU-api/src/entities/grader/grader.controller.ts @@ -20,4 +20,16 @@ export async function grade(req: Request, res: Response, next: NextFunction) { } } -export default { grade } +export async function tangoCallback(req: Request, res: Response, next: NextFunction) { + console.log("haiiiii hiiiii :3") + try { + const submissionId = parseInt(req.params.id) + const response = await GraderService.tangoCallback(submissionId) + + res.status(200).json(response) + } catch (err) { + res.status(400).json(new GenericResponse(err.message)) + } +} + +export default { grade, tangoCallback } diff --git a/devU-api/src/entities/grader/grader.router.ts b/devU-api/src/entities/grader/grader.router.ts index 1ed309a8..2ae38d63 100644 --- a/devU-api/src/entities/grader/grader.router.ts +++ b/devU-api/src/entities/grader/grader.router.ts @@ -32,4 +32,6 @@ const Router = express.Router() */ Router.post('/:id', asInt(), GraderController.grade) +Router.post('/callback/:id', asInt(), GraderController.tangoCallback) + export default Router \ No newline at end of file diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index 1d3848bb..75fbc9a2 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -6,7 +6,7 @@ import containerAutograderService from '../containerAutoGrader/containerAutoGrad import assignmentProblemService from '../assignmentProblem/assignmentProblem.service' import assignmentScoreService from '../assignmentScore/assignmentScore.service' import courseService from '../course/course.service' -import { addJob, openDirectory, uploadFile } from '../../tango/tango.service' +import { addJob, createCourse, uploadFile } from '../../tango/tango.service' import { SubmissionScore, SubmissionProblemScore, ContainerAutoGrader, AssignmentScore } from 'devu-shared-modules' import { checkAnswer } from '../nonContainerAutoGrader/nonContainerAutoGrader.grader' @@ -15,6 +15,7 @@ import { serialize as serializeAssignmentScore } from '../assignmentScore/assign import { downloadFile, initializeMinio } from '../../fileStorage' import crypto from 'crypto' +import environment from '../../environment' export async function grade(submissionId: number) { const submission = await submissionService.retrieve(submissionId) @@ -66,7 +67,7 @@ export async function grade(submissionId: number) { var response = null const labName = bucketName + '-' + submission.assignmentId const optionFiles = [] - const openResponse = await openDirectory(labName) + const openResponse = await createCourse(labName) if (openResponse) { if (!(openResponse.files["Graderfile"]) || openResponse.files["Graderfile"] !== crypto.createHash('md5').update(graderData).digest('hex')) { await uploadFile(labName, graderData, "Graderfile") @@ -80,15 +81,17 @@ export async function grade(submissionId: number) { optionFiles.push({localFile: filepath, destFile: filepath}) } } + console.log(environment.apiUrl) + console.log(labName) const jobOptions = { image: autogradingImage, - files: [{localFile: "Graderfile", destFile: "Graderfile"}, + files: [{localFile: "Graderfile", destFile: "autograde.tar"}, {localFile: "Makefile", destFile: "Makefile"},] .concat(optionFiles), jobName: labName, output_file: labName, timeout: timeout, - callback_url: "" + callback_url: `${environment.apiUrl}/grade/callback/${submissionId}` } response = await addJob(labName, jobOptions) } @@ -140,4 +143,10 @@ export async function mockContainerCheckAnswer(file: string, containerAutoGrader return gradeResults } -export default { grade } \ No newline at end of file +export async function tangoCallback(submissionId: number) { + console.log("!!!!!!!!!!!!!!YAYYYY!!!!!!!!!!!!!!\n!!!!!!!!!!!!!!YAYYYY!!!!!!!!!!!!!!\n!!!!!!!!!!!!!!YAYYYY!!!!!!!!!!!!!!\n") + console.log(`${submissionId} did it yayyy`) + return {id: submissionId} +} + +export default { grade, tangoCallback } \ No newline at end of file diff --git a/devU-api/src/tango/tango.service.ts b/devU-api/src/tango/tango.service.ts index cf720a53..33dfe851 100644 --- a/devU-api/src/tango/tango.service.ts +++ b/devU-api/src/tango/tango.service.ts @@ -1,7 +1,7 @@ import './tango.types' import fetch from "node-fetch"; -const tangoHost = `http://127.0.0.1:3000` +const tangoHost = `http://tango:3000` const tangoKey = process.env.TANGO_KEY ?? 'test' // for more info https://docs.autolabproject.com/tango-rest/ @@ -22,11 +22,9 @@ export async function createCourse(course: string): Promise * @param fileName - The file name, used to identify the file when uploaded * @param file - The file to be uploaded. */ -export async function uploadFile(course: string, file: File, fileName: string): Promise { +export async function uploadFile(course: string, file: Buffer, fileName: string): Promise { const url = `${tangoHost}/upload/${tangoKey}/${course}/` - const formData = new FormData() - formData.append('file', file) - const response = await fetch(url, { method: 'POST', body: formData, headers: { 'filename': fileName } }) + const response = await fetch(url, { method: 'POST', body: file, headers: { 'filename': fileName } }) return response.ok ? await response.json() as UploadResponse : null } diff --git a/docker-compose.yml b/docker-compose.yml index f1816242..833d2733 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,7 +91,7 @@ services: environment: - DOCKER_REDIS_HOSTNAME=redis - RESTFUL_KEY=devutangokey - - DOCKER_TANGO_HOST_VOLUME_PATH= #TODO add the absolute path to tango_files here before running + - DOCKER_TANGO_HOST_VOLUME_PATH= C:\Users\Keifer\Desktop\Programming\devU\tango_files #TODO add the absolute path to tango_files here before running depends_on: - redis @@ -101,6 +101,7 @@ services: - ./logs/tango/:/var/log/tango/ - ./logs/tangonginx:/var/log/nginx - ./tango_files/volumes:/opt/TangoService/Tango/volumes + # certbot: # container_name: certbot From 0ca1928c4d1a82f329f61eaf60de3ad7c69f81c6 Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Thu, 25 Apr 2024 20:05:36 -0400 Subject: [PATCH 10/16] Callback http request is now successfully sent and recieved --- devU-api/src/entities/grader/grader.router.ts | 5 +++-- devU-api/src/entities/grader/grader.service.ts | 2 +- devU-api/src/router/index.ts | 2 +- docker-compose.yml | 5 +++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/devU-api/src/entities/grader/grader.router.ts b/devU-api/src/entities/grader/grader.router.ts index 2ae38d63..0df60a4c 100644 --- a/devU-api/src/entities/grader/grader.router.ts +++ b/devU-api/src/entities/grader/grader.router.ts @@ -4,6 +4,7 @@ import express from 'express' // Middleware //import validator from './grader.validator' import { asInt } from '../../middleware/validator/generic.validator' +import { isAuthorized } from '../../auth/auth.middleware' // Controller import GraderController from './grader.controller' @@ -30,8 +31,8 @@ const Router = express.Router() * schema: * type: integer */ -Router.post('/:id', asInt(), GraderController.grade) +Router.post('/:id', asInt(), isAuthorized, GraderController.grade) -Router.post('/callback/:id', asInt(), GraderController.tangoCallback) +Router.post('/callback/:id', asInt(), GraderController.tangoCallback) //Unauthorized route so tango can make callback without needing token export default Router \ No newline at end of file diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index 75fbc9a2..692f30e3 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -91,7 +91,7 @@ export async function grade(submissionId: number) { jobName: labName, output_file: labName, timeout: timeout, - callback_url: `${environment.apiUrl}/grade/callback/${submissionId}` + callback_url: `http://api:3001/grade/callback/${submissionId}` } response = await addJob(labName, jobOptions) } diff --git a/devU-api/src/router/index.ts b/devU-api/src/router/index.ts index 76e85ced..f352eba2 100644 --- a/devU-api/src/router/index.ts +++ b/devU-api/src/router/index.ts @@ -42,7 +42,7 @@ Router.use('/container-auto-graders', isAuthorized, containerAutoGrader) Router.use('/submission-problem-scores', isAuthorized, submissionProblemScore) Router.use('/file-upload', isAuthorized, fileUpload) Router.use('/deadline-extensions', isAuthorized, deadlineExtensions) -Router.use('/grade', isAuthorized, grader) +Router.use('/grade', grader) //Only the /grade/callback/:id route isn't authorized Router.use('/categories', isAuthorized, categories) Router.use('/assignment-scores', isAuthorized, assignmentScore) diff --git a/docker-compose.yml b/docker-compose.yml index 833d2733..5070e2ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,7 @@ services: - ./devU-api/config:/config api: # Runs the API + container_name: api build: context: . dockerfile: api.Dockerfile @@ -91,7 +92,7 @@ services: environment: - DOCKER_REDIS_HOSTNAME=redis - RESTFUL_KEY=devutangokey - - DOCKER_TANGO_HOST_VOLUME_PATH= C:\Users\Keifer\Desktop\Programming\devU\tango_files #TODO add the absolute path to tango_files here before running + - DOCKER_TANGO_HOST_VOLUME_PATH=C:\Users\Keifer\Desktop\Programming\devU\tango_files #TODO add the absolute path to tango_files here before running depends_on: - redis @@ -100,7 +101,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - ./logs/tango/:/var/log/tango/ - ./logs/tangonginx:/var/log/nginx - - ./tango_files/volumes:/opt/TangoService/Tango/volumes + - ./tango_files:/opt/TangoService/Tango/volumes # certbot: From 6b883ca32a7078c3c5c63a28f222e1f6d9216900 Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Fri, 26 Apr 2024 03:24:48 -0400 Subject: [PATCH 11/16] Updated tango pollJob, container grading callback mostly complete --- .../src/entities/grader/grader.controller.ts | 5 +- devU-api/src/entities/grader/grader.router.ts | 2 +- .../src/entities/grader/grader.service.ts | 61 ++++++++++++------- devU-api/src/tango/tango.service.ts | 10 +-- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/devU-api/src/entities/grader/grader.controller.ts b/devU-api/src/entities/grader/grader.controller.ts index 1d08e5b7..830ca6dc 100644 --- a/devU-api/src/entities/grader/grader.controller.ts +++ b/devU-api/src/entities/grader/grader.controller.ts @@ -21,13 +21,14 @@ export async function grade(req: Request, res: Response, next: NextFunction) { } export async function tangoCallback(req: Request, res: Response, next: NextFunction) { - console.log("haiiiii hiiiii :3") try { - const submissionId = parseInt(req.params.id) + console.log('awawawa') + const submissionId = req.params.id const response = await GraderService.tangoCallback(submissionId) res.status(200).json(response) } catch (err) { + console.log("wah :((") res.status(400).json(new GenericResponse(err.message)) } } diff --git a/devU-api/src/entities/grader/grader.router.ts b/devU-api/src/entities/grader/grader.router.ts index 0df60a4c..b0fc26b9 100644 --- a/devU-api/src/entities/grader/grader.router.ts +++ b/devU-api/src/entities/grader/grader.router.ts @@ -33,6 +33,6 @@ const Router = express.Router() */ Router.post('/:id', asInt(), isAuthorized, GraderController.grade) -Router.post('/callback/:id', asInt(), GraderController.tangoCallback) //Unauthorized route so tango can make callback without needing token +Router.post('/callback/:id', GraderController.tangoCallback) //Unauthorized route so tango can make callback without needing token export default Router \ No newline at end of file diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index 692f30e3..622b5d6a 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -6,9 +6,9 @@ import containerAutograderService from '../containerAutoGrader/containerAutoGrad import assignmentProblemService from '../assignmentProblem/assignmentProblem.service' import assignmentScoreService from '../assignmentScore/assignmentScore.service' import courseService from '../course/course.service' -import { addJob, createCourse, uploadFile } from '../../tango/tango.service' +import { addJob, createCourse, uploadFile, pollJob } from '../../tango/tango.service' -import { SubmissionScore, SubmissionProblemScore, ContainerAutoGrader, AssignmentScore } from 'devu-shared-modules' +import { SubmissionScore, SubmissionProblemScore, AssignmentScore } from 'devu-shared-modules' import { checkAnswer } from '../nonContainerAutoGrader/nonContainerAutoGrader.grader' import { serialize as serializeNonContainer } from '../nonContainerAutoGrader/nonContainerAutoGrader.serializer' import { serialize as serializeAssignmentScore } from '../assignmentScore/assignmentScore.serializer' @@ -65,7 +65,7 @@ export async function grade(submissionId: number) { initializeMinio(bucketName) var response = null - const labName = bucketName + '-' + submission.assignmentId + const labName = `${bucketName}-${submission.assignmentId}` const optionFiles = [] const openResponse = await createCourse(labName) if (openResponse) { @@ -88,15 +88,15 @@ export async function grade(submissionId: number) { files: [{localFile: "Graderfile", destFile: "autograde.tar"}, {localFile: "Makefile", destFile: "Makefile"},] .concat(optionFiles), - jobName: labName, - output_file: labName, + jobName: `${labName}-${submissionId}`, + output_file: `${labName}-${submissionId}-output.txt`, timeout: timeout, - callback_url: `http://api:3001/grade/callback/${submissionId}` + callback_url: `http://api:3001/grade/callback/${labName}-${submissionId}-output.txt` } response = await addJob(labName, jobOptions) } } catch (e) { - console.log(e) + console.error(e) } //remember, immediate callback is made when job has been added to queue, not sure how we're handling the rest of it yet though lmao @@ -127,26 +127,41 @@ export async function grade(submissionId: number) { return response } -//Temporary mock function, delete when the container autograder grading function is written -export async function mockContainerCheckAnswer(file: string, containerAutoGrader: ContainerAutoGrader) { - let gradeResults = [] - //SubmissionProblemScore 1 - gradeResults.push({score: 5, feedback: "Grader #" + containerAutoGrader.id + " graded " + file + " problem 1 for 5/5 points"}) +export async function tangoCallback(outputFile: string) { + console.log('goot!') + //Output filename consists of 4 sections separated by hyphens. + and () only for visual clarity, not a part of the filename + //(course.number+course.semester+course.id)-(assignment.id)-(submission.id)-(output.txt) + const filenameSplit = outputFile.split('-') + const labName = `${filenameSplit[0]}-${filenameSplit[1]}` + const assignmentId = Number(filenameSplit[1]) + const submissionId = Number(filenameSplit[2]) - //SubmissionProblemScore 2 - gradeResults.push({score: 5, feedback: "Grader #" + containerAutoGrader.id + " graded " + file + " problem 2 for 5/5 points"}) + const response = await pollJob(labName, outputFile) + if (typeof response !== 'string') { + throw new Error('Autograder output file not found') + } + const splitResponse = (response as string).split(/\r\n|\r|\n/) + const scores = JSON.parse(splitResponse[splitResponse.length - 2]) - //SubmissionProblemScore 3 - gradeResults.push({score: 10, feedback: "Grader #" + containerAutoGrader.id + " graded " + file + " problem 3 for 10/10 points"}) - - return gradeResults -} + let score = 0 + const assignmentProblems = await assignmentProblemService.list(assignmentId) + for (const question in scores['scores']) { + const assignmentProblem = assignmentProblems.find(problem => problem.problemName === question) + if (assignmentProblem) { + const problemScoreObj: SubmissionProblemScore = { + submissionId: submissionId, + assignmentProblemId: assignmentProblem.id, + score: Number(scores[question]), + feedback: '' //Not sure what to do for individual problemscore feedback + } + submissionProblemScoreService.create(problemScoreObj) + score += Number(scores[question]) + } + + } -export async function tangoCallback(submissionId: number) { - console.log("!!!!!!!!!!!!!!YAYYYY!!!!!!!!!!!!!!\n!!!!!!!!!!!!!!YAYYYY!!!!!!!!!!!!!!\n!!!!!!!!!!!!!!YAYYYY!!!!!!!!!!!!!!\n") - console.log(`${submissionId} did it yayyy`) - return {id: submissionId} + return {output: response} } export default { grade, tangoCallback } \ No newline at end of file diff --git a/devU-api/src/tango/tango.service.ts b/devU-api/src/tango/tango.service.ts index 33dfe851..55f2203a 100644 --- a/devU-api/src/tango/tango.service.ts +++ b/devU-api/src/tango/tango.service.ts @@ -48,13 +48,13 @@ export async function addJob(course: string, job: AddJobRequest): Promise { +export async function pollJob(course: string, outputFile: string): Promise { //PollSuccessResponse const url = `${tangoHost}/poll/${tangoKey}/${course}/${outputFile}/` const response = await fetch(url, { method: 'GET' }) - const data = await response.json() - return response.ok ? - data as PollSuccessResponse - : data as PollFailureResponse + + return response.headers.get('Content-Type')?.includes('application/json') + ? (await response.json()) as PollFailureResponse + : (await response.text()) as PollSuccessResponse } /** From 494cf41f6404900f26ea3b8c655f99857f888056 Mon Sep 17 00:00:00 2001 From: Keiferms3 Date: Fri, 26 Apr 2024 11:11:55 -0400 Subject: [PATCH 12/16] Small changes to grader service during meeting --- .../src/entities/grader/grader.service.ts | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index 622b5d6a..4266125f 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -141,12 +141,13 @@ export async function tangoCallback(outputFile: string) { if (typeof response !== 'string') { throw new Error('Autograder output file not found') } - const splitResponse = (response as string).split(/\r\n|\r|\n/) - const scores = JSON.parse(splitResponse[splitResponse.length - 2]) + const splitResponse = response.split(/\r\n|\r|\n/) + const scores = (JSON.parse(splitResponse[splitResponse.length - 2])).scores let score = 0 const assignmentProblems = await assignmentProblemService.list(assignmentId) - for (const question in scores['scores']) { + const submissionScore = await submissionScoreService.retrieve(submissionId) + for (const question in scores) { const assignmentProblem = assignmentProblems.find(problem => problem.problemName === question) if (assignmentProblem) { const problemScoreObj: SubmissionProblemScore = { @@ -158,7 +159,19 @@ export async function tangoCallback(outputFile: string) { submissionProblemScoreService.create(problemScoreObj) score += Number(scores[question]) } - + } + if (submissionScore) { + submissionScore.score += score + submissionScore.feedback += `\n${response}` + + submissionScoreService.update(submissionScore) + } else { + const scoreObj: SubmissionScore = { + submissionId: submissionId, + score: score, //Sum of all SubmissionProblemScore scores + feedback: response //Feedback from Tango + } + submissionScoreService.create(scoreObj) } return {output: response} From 92a53cd92800e68e605d34bbadbde77d219b3c62 Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Fri, 26 Apr 2024 12:48:49 -0400 Subject: [PATCH 13/16] Container autograding functional --- .../containerAutoGrader.service.ts | 2 +- .../src/entities/grader/grader.service.ts | 147 ++++++++++-------- 2 files changed, 81 insertions(+), 68 deletions(-) diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts index 716868ad..1a2eba31 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts @@ -93,7 +93,7 @@ export async function getGraderObjectByAssignmentId(assignmentId: number) { export async function getGraderByAssignmentId(assignmentId: number){ const containerAutoGraders = await connect().findOne({ assignmentId: assignmentId, deletedAt: IsNull() }) - if (!containerAutoGraders) throw new Error('No containerAutoGraders found') + if (!containerAutoGraders) return {graderData: null, makefileData: null, autogradingImage: null, timeout: null} const { graderFile, makefileFile, autogradingImage, timeout } = containerAutoGraders const graderData = await downloadFile('graders', graderFile) diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index 4266125f..b5320f3e 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -8,18 +8,20 @@ import assignmentScoreService from '../assignmentScore/assignmentScore.service' import courseService from '../course/course.service' import { addJob, createCourse, uploadFile, pollJob } from '../../tango/tango.service' -import { SubmissionScore, SubmissionProblemScore, AssignmentScore } from 'devu-shared-modules' +import { SubmissionScore, SubmissionProblemScore, AssignmentScore, Submission } from 'devu-shared-modules' import { checkAnswer } from '../nonContainerAutoGrader/nonContainerAutoGrader.grader' import { serialize as serializeNonContainer } from '../nonContainerAutoGrader/nonContainerAutoGrader.serializer' import { serialize as serializeAssignmentScore } from '../assignmentScore/assignmentScore.serializer' +import { serialize as serializeSubmissionScore} from '../submissionScore/submissionScore.serializer' +import { serialize as serializeSubmission } from '../submission/submission.serializer' import { downloadFile, initializeMinio } from '../../fileStorage' -import crypto from 'crypto' import environment from '../../environment' export async function grade(submissionId: number) { - const submission = await submissionService.retrieve(submissionId) - if (!submission) return null + const submissionModel = await submissionService.retrieve(submissionId) + if (!submissionModel) throw new Error('Submission not found.') + const submission = serializeSubmission(submissionModel) const assignmentId = submission.assignmentId @@ -35,6 +37,7 @@ export async function grade(submissionId: number) { let score = 0 let feedback = '' let allScores = [] //This is the return value, the serializer parses it into a GraderInfo object for the controller to return + let containerGrading = true //Is set to false if no container autograders are found for this assignment //Run Non-Container Autograders for (const question in form) { @@ -57,48 +60,48 @@ export async function grade(submissionId: number) { } //Run Container Autograders - try { - const {graderData, makefileData, autogradingImage, timeout} = await containerAutograderService.getGraderByAssignmentId(assignmentId) - const bucketName = await courseService.retrieve(submission.courseId).then((course) => { - return course ? (course.number + course.semester + course.id).toLowerCase() : 'submission' - }) - initializeMinio(bucketName) - - var response = null - const labName = `${bucketName}-${submission.assignmentId}` - const optionFiles = [] - const openResponse = await createCourse(labName) - if (openResponse) { - if (!(openResponse.files["Graderfile"]) || openResponse.files["Graderfile"] !== crypto.createHash('md5').update(graderData).digest('hex')) { + const {graderData, makefileData, autogradingImage, timeout} = await containerAutograderService.getGraderByAssignmentId(assignmentId) + if (!graderData || !makefileData || !autogradingImage || !timeout) { + containerGrading = false + } else { + try { + const bucketName = await courseService.retrieve(submission.courseId).then((course) => { + return course ? (course.number + course.semester + course.id).toLowerCase() : 'submission' + }) + initializeMinio(bucketName) + + var response = null + const labName = `${bucketName}-${submission.assignmentId}` + const optionFiles = [] + const openResponse = await createCourse(labName) + if (openResponse) { await uploadFile(labName, graderData, "Graderfile") - } - if (!(openResponse.files["Makefile"]) || openResponse.files["Makefile"] !== crypto.createHash('md5').update(makefileData).digest('hex')) { await uploadFile(labName, makefileData, "Makefile") - } - for (const filepath of filepaths){ - const buffer = await downloadFile(bucketName, filepath) - if (await uploadFile(labName, buffer, filepath)) { - optionFiles.push({localFile: filepath, destFile: filepath}) + + for (const filepath of filepaths){ + const buffer = await downloadFile(bucketName, filepath) + if (await uploadFile(labName, buffer, filepath)) { + optionFiles.push({localFile: filepath, destFile: filepath}) + } } + console.log(environment.apiUrl) + console.log(labName) + const jobOptions = { + image: autogradingImage, + files: [{localFile: "Graderfile", destFile: "autograde.tar"}, + {localFile: "Makefile", destFile: "Makefile"},] + .concat(optionFiles), + jobName: `${labName}-${submissionId}`, + output_file: `${labName}-${submissionId}-output.txt`, + timeout: timeout, + callback_url: `http://api:3001/grade/callback/${labName}-${submissionId}-output.txt` + } + response = await addJob(labName, jobOptions) } - console.log(environment.apiUrl) - console.log(labName) - const jobOptions = { - image: autogradingImage, - files: [{localFile: "Graderfile", destFile: "autograde.tar"}, - {localFile: "Makefile", destFile: "Makefile"},] - .concat(optionFiles), - jobName: `${labName}-${submissionId}`, - output_file: `${labName}-${submissionId}-output.txt`, - timeout: timeout, - callback_url: `http://api:3001/grade/callback/${labName}-${submissionId}-output.txt` - } - response = await addJob(labName, jobOptions) + } catch (e: any) { + throw new Error(e) } - } catch (e) { - console.error(e) } - //remember, immediate callback is made when job has been added to queue, not sure how we're handling the rest of it yet though lmao //Grading is finished. Create SubmissionScore and AssignmentScore and save to db. const scoreObj: SubmissionScore = { @@ -108,28 +111,14 @@ export async function grade(submissionId: number) { } allScores.push(await submissionScoreService.create(scoreObj)) - //PLACEHOLDER AssignmentScore logic. This should be customizable, but for now AssignmentScore will simply equal the latest SubmissionScore - const assignmentScoreModel = await assignmentScoreService.retrieveByUser(submission.assignmentId, submission.userId) - if (assignmentScoreModel) { //If assignmentScore already exists, update existing entity - const assignmentScore = serializeAssignmentScore(assignmentScoreModel) - assignmentScore.score = score - assignmentScoreService.update(assignmentScore) - - } else { //Otherwise make a new one - const assignmentScore: AssignmentScore = { - assignmentId: submission.assignmentId, - userId: submission.userId, - score: score, - } - await assignmentScoreService.create(assignmentScore) - } + //If containergrading is true, tangoCallback handles assignmentScore creation + if (containerGrading === false) updateAssignmentScore(submission, score) return response } export async function tangoCallback(outputFile: string) { - console.log('goot!') //Output filename consists of 4 sections separated by hyphens. + and () only for visual clarity, not a part of the filename //(course.number+course.semester+course.id)-(assignment.id)-(submission.id)-(output.txt) const filenameSplit = outputFile.split('-') @@ -138,15 +127,18 @@ export async function tangoCallback(outputFile: string) { const submissionId = Number(filenameSplit[2]) const response = await pollJob(labName, outputFile) - if (typeof response !== 'string') { - throw new Error('Autograder output file not found') - } + if (typeof response !== 'string') throw new Error('Autograder output file not found') + const splitResponse = response.split(/\r\n|\r|\n/) const scores = (JSON.parse(splitResponse[splitResponse.length - 2])).scores let score = 0 const assignmentProblems = await assignmentProblemService.list(assignmentId) - const submissionScore = await submissionScoreService.retrieve(submissionId) + const submissionScoreModel = await submissionScoreService.retrieve(submissionId) + const submissionModel = await submissionService.retrieve(submissionId) + if (!submissionModel) throw new Error("Submission not found.") + const submission = serializeSubmission(submissionModel) + for (const question in scores) { const assignmentProblem = assignmentProblems.find(problem => problem.problemName === question) if (assignmentProblem) { @@ -160,21 +152,42 @@ export async function tangoCallback(outputFile: string) { score += Number(scores[question]) } } - if (submissionScore) { - submissionScore.score += score + if (submissionScoreModel) { //If noncontainer grading has occured + var submissionScore = serializeSubmissionScore(submissionScoreModel) + submissionScore.score = (submissionScore.score ?? 0) + score + score = submissionScore.score submissionScore.feedback += `\n${response}` - submissionScoreService.update(submissionScore) - } else { - const scoreObj: SubmissionScore = { + await submissionScoreService.update(submissionScore) + } else { //If submission is exclusively container graded + var submissionScore: SubmissionScore = { submissionId: submissionId, score: score, //Sum of all SubmissionProblemScore scores feedback: response //Feedback from Tango } - submissionScoreService.create(scoreObj) + await submissionScoreService.create(submissionScore) } + await updateAssignmentScore(submission, score) + + return (submissionScore) +} + +//Currently just sets assignmentscore to the latest submission. Pulled this function out for easy future modification. +async function updateAssignmentScore(submission: Submission, score: number) { + const assignmentScoreModel = await assignmentScoreService.retrieveByUser(submission.assignmentId, submission.userId) + if (assignmentScoreModel) { //If assignmentScore already exists, update existing entity + const assignmentScore = serializeAssignmentScore(assignmentScoreModel) + assignmentScore.score = score + await assignmentScoreService.update(assignmentScore) - return {output: response} + } else { //Otherwise create a new one + const assignmentScore: AssignmentScore = { + assignmentId: submission.assignmentId, + userId: submission.userId, + score: score, + } + await assignmentScoreService.create(assignmentScore) + } } export default { grade, tangoCallback } \ No newline at end of file From 2226c6059b1d04ea54003fcdffd89921f7590ebc Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Fri, 26 Apr 2024 14:42:40 -0400 Subject: [PATCH 14/16] Container autograding now handles invalid files properly, improved grader feedback --- .../src/entities/grader/grader.controller.ts | 9 +- devU-api/src/entities/grader/grader.router.ts | 2 +- .../src/entities/grader/grader.service.ts | 102 ++++++++++++------ .../nonContainerAutoGrader.grader.ts | 6 +- 4 files changed, 78 insertions(+), 41 deletions(-) diff --git a/devU-api/src/entities/grader/grader.controller.ts b/devU-api/src/entities/grader/grader.controller.ts index 830ca6dc..fd3d5d10 100644 --- a/devU-api/src/entities/grader/grader.controller.ts +++ b/devU-api/src/entities/grader/grader.controller.ts @@ -10,9 +10,6 @@ export async function grade(req: Request, res: Response, next: NextFunction) { try { const submissionId = parseInt(req.params.id) const response = await GraderService.grade(submissionId) //grade - //if (!grade || grade.length === 0) return res.status(404).json(NotFound) - - //const response = serialize(grade) res.status(200).json({message: response?.jobId + ", " + response?.statusId + ", " + response?.statusMsg}) } catch (err) { @@ -22,13 +19,11 @@ export async function grade(req: Request, res: Response, next: NextFunction) { export async function tangoCallback(req: Request, res: Response, next: NextFunction) { try { - console.log('awawawa') - const submissionId = req.params.id - const response = await GraderService.tangoCallback(submissionId) + const outputFile = req.params.outputFile + const response = await GraderService.tangoCallback(outputFile) res.status(200).json(response) } catch (err) { - console.log("wah :((") res.status(400).json(new GenericResponse(err.message)) } } diff --git a/devU-api/src/entities/grader/grader.router.ts b/devU-api/src/entities/grader/grader.router.ts index b0fc26b9..90f6f88d 100644 --- a/devU-api/src/entities/grader/grader.router.ts +++ b/devU-api/src/entities/grader/grader.router.ts @@ -33,6 +33,6 @@ const Router = express.Router() */ Router.post('/:id', asInt(), isAuthorized, GraderController.grade) -Router.post('/callback/:id', GraderController.tangoCallback) //Unauthorized route so tango can make callback without needing token +Router.post('/callback/:outputFile', GraderController.tangoCallback) //Unauthorized route so tango can make callback without needing token export default Router \ No newline at end of file diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index b5320f3e..a26686ce 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -18,7 +18,7 @@ import { downloadFile, initializeMinio } from '../../fileStorage' import environment from '../../environment' -export async function grade(submissionId: number) { +async function grade(submissionId: number) { const submissionModel = await submissionService.retrieve(submissionId) if (!submissionModel) throw new Error('Submission not found.') const submission = serializeSubmission(submissionModel) @@ -27,10 +27,9 @@ export async function grade(submissionId: number) { const content = JSON.parse(submission.content) const form = content.form - const filepaths: string[] = content.filepaths //Using the field name that was written on the whiteboard for now + const filepaths: string[] = content.filepaths const nonContainerAutograders = await nonContainerAutograderService.listByAssignmentId(assignmentId) - //const containerAutograders = await containerAutograderService.listByAssignmentId(assignmentId) const assignmentProblems = await assignmentProblemService.list(assignmentId) @@ -118,7 +117,7 @@ export async function grade(submissionId: number) { } -export async function tangoCallback(outputFile: string) { +async function tangoCallback(outputFile: string) { //Output filename consists of 4 sections separated by hyphens. + and () only for visual clarity, not a part of the filename //(course.number+course.semester+course.id)-(assignment.id)-(submission.id)-(output.txt) const filenameSplit = outputFile.split('-') @@ -126,50 +125,93 @@ export async function tangoCallback(outputFile: string) { const assignmentId = Number(filenameSplit[1]) const submissionId = Number(filenameSplit[2]) - const response = await pollJob(labName, outputFile) - if (typeof response !== 'string') throw new Error('Autograder output file not found') + try { + const response = await pollJob(labName, outputFile) + if (typeof response !== 'string') throw 'Autograder output file not found' - const splitResponse = response.split(/\r\n|\r|\n/) - const scores = (JSON.parse(splitResponse[splitResponse.length - 2])).scores + try { + const splitResponse = response.split(/\r\n|\r|\n/) + var scoresLine = JSON.parse(splitResponse[splitResponse.length - 2]) + } catch { + throw response + } + const scores = scoresLine.scores + + let score = 0 + const assignmentProblems = await assignmentProblemService.list(assignmentId) + const submissionScoreModel = await submissionScoreService.retrieve(submissionId) + const submissionModel = await submissionService.retrieve(submissionId) + if (!submissionModel) throw "Submission not found." + const submission = serializeSubmission(submissionModel) + + for (const question in scores) { + const assignmentProblem = assignmentProblems.find(problem => problem.problemName === question) + if (assignmentProblem) { + const problemScoreObj: SubmissionProblemScore = { + submissionId: submissionId, + assignmentProblemId: assignmentProblem.id, + score: Number(scores[question]), + feedback: `Autograder graded ${assignmentProblem.problemName} for ${Number(scores[question])} points` + } + submissionProblemScoreService.create(problemScoreObj) + score += Number(scores[question]) + } + } + if (submissionScoreModel) { //If noncontainer grading has occured + var submissionScore = serializeSubmissionScore(submissionScoreModel) + submissionScore.score = (submissionScore.score ?? 0) + score + score = submissionScore.score + submissionScore.feedback += `\n${response}` + + await submissionScoreService.update(submissionScore) + } else { //If submission is exclusively container graded + var submissionScore: SubmissionScore = { + submissionId: submissionId, + score: score, //Sum of all SubmissionProblemScore scores + feedback: response //Feedback from Tango + } + await submissionScoreService.create(submissionScore) + } + await updateAssignmentScore(submission, score) - let score = 0 + return {submissionScore: submissionScore, outputFile: response} + } catch (e: any) { + callbackFailure(assignmentId, submissionId, e) + throw new Error(e) + } +} + +export async function callbackFailure(assignmentId: number, submissionId: number, file: string) { const assignmentProblems = await assignmentProblemService.list(assignmentId) const submissionScoreModel = await submissionScoreService.retrieve(submissionId) - const submissionModel = await submissionService.retrieve(submissionId) - if (!submissionModel) throw new Error("Submission not found.") - const submission = serializeSubmission(submissionModel) + const submissionProblemScoreModels = await submissionProblemScoreService.list(submissionId) - for (const question in scores) { - const assignmentProblem = assignmentProblems.find(problem => problem.problemName === question) - if (assignmentProblem) { + for (const assignmentProblem of assignmentProblems) { + const submissionProblemScore = submissionProblemScoreModels.find((sps) => sps.assignmentProblemId === assignmentProblem.id) + if (!submissionProblemScore) { //If assignmentProblem hasn't already been graded by noncontainer autograder const problemScoreObj: SubmissionProblemScore = { submissionId: submissionId, assignmentProblemId: assignmentProblem.id, - score: Number(scores[question]), - feedback: '' //Not sure what to do for individual problemscore feedback + score: 0, + feedback: 'Autograding failed to complete.' } submissionProblemScoreService.create(problemScoreObj) - score += Number(scores[question]) } } if (submissionScoreModel) { //If noncontainer grading has occured var submissionScore = serializeSubmissionScore(submissionScoreModel) - submissionScore.score = (submissionScore.score ?? 0) + score - score = submissionScore.score - submissionScore.feedback += `\n${response}` + submissionScore.score = (submissionScore.score ?? 0) + submissionScore.feedback += `\n${file}` - await submissionScoreService.update(submissionScore) + submissionScoreService.update(submissionScore) } else { //If submission is exclusively container graded var submissionScore: SubmissionScore = { submissionId: submissionId, - score: score, //Sum of all SubmissionProblemScore scores - feedback: response //Feedback from Tango + score: 0, + feedback: file } - await submissionScoreService.create(submissionScore) + submissionScoreService.create(submissionScore) } - await updateAssignmentScore(submission, score) - - return (submissionScore) } //Currently just sets assignmentscore to the latest submission. Pulled this function out for easy future modification. @@ -178,7 +220,7 @@ async function updateAssignmentScore(submission: Submission, score: number) { if (assignmentScoreModel) { //If assignmentScore already exists, update existing entity const assignmentScore = serializeAssignmentScore(assignmentScoreModel) assignmentScore.score = score - await assignmentScoreService.update(assignmentScore) + assignmentScoreService.update(assignmentScore) } else { //Otherwise create a new one const assignmentScore: AssignmentScore = { @@ -186,7 +228,7 @@ async function updateAssignmentScore(submission: Submission, score: number) { userId: submission.userId, score: score, } - await assignmentScoreService.create(assignmentScore) + assignmentScoreService.create(assignmentScore) } } diff --git a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts index 05cab5e9..fea1ef50 100644 --- a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts +++ b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts @@ -11,15 +11,15 @@ export function checkAnswer(studentAnswer: string, nonContainerAutoGrader: NonCo const isMatch: boolean = pattern.test(studentAnswer) if (isMatch) { - return [nonContainerAutoGrader.score, `Grader #${nonContainerAutoGrader.id} graded "${nonContainerAutoGrader.question}" for ${nonContainerAutoGrader.score} points`] + return [nonContainerAutoGrader.score, `Autograder graded "${nonContainerAutoGrader.question}" for ${nonContainerAutoGrader.score} points`] } } else { // if no regex is set use normal string matching if (studentAnswer === nonContainerAutoGrader.correctString) { - return [nonContainerAutoGrader.score, `Grader #${nonContainerAutoGrader.id} graded "${nonContainerAutoGrader.question}" for ${nonContainerAutoGrader.score} points`] + return [nonContainerAutoGrader.score, `Autograder graded "${nonContainerAutoGrader.question}" for ${nonContainerAutoGrader.score} points`] } } // default value to return if all conditions fail to execute // i.e. the answer is incorrect or improperly formatted - return [0, `Grader #${nonContainerAutoGrader.id} graded "${nonContainerAutoGrader.question}" for 0 points`] + return [0, `Autograder graded "${nonContainerAutoGrader.question}" for 0 points`] } \ No newline at end of file From 416956f1eb0e8e5704ac2ee22675dfebb8a9158e Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Fri, 26 Apr 2024 15:15:16 -0400 Subject: [PATCH 15/16] Improved response messages --- .../src/entities/grader/grader.controller.ts | 2 +- devU-api/src/entities/grader/grader.router.ts | 20 +++++++++++++++++++ .../src/entities/grader/grader.service.ts | 17 ++++++++-------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/devU-api/src/entities/grader/grader.controller.ts b/devU-api/src/entities/grader/grader.controller.ts index fd3d5d10..f8f18370 100644 --- a/devU-api/src/entities/grader/grader.controller.ts +++ b/devU-api/src/entities/grader/grader.controller.ts @@ -11,7 +11,7 @@ export async function grade(req: Request, res: Response, next: NextFunction) { const submissionId = parseInt(req.params.id) const response = await GraderService.grade(submissionId) //grade - res.status(200).json({message: response?.jobId + ", " + response?.statusId + ", " + response?.statusMsg}) + res.status(200).json(response) } catch (err) { res.status(400).json(new GenericResponse(err.message)) } diff --git a/devU-api/src/entities/grader/grader.router.ts b/devU-api/src/entities/grader/grader.router.ts index 90f6f88d..2500fa8f 100644 --- a/devU-api/src/entities/grader/grader.router.ts +++ b/devU-api/src/entities/grader/grader.router.ts @@ -33,6 +33,26 @@ const Router = express.Router() */ Router.post('/:id', asInt(), isAuthorized, GraderController.grade) +/** + * @swagger + * tags: + * - name: Grader callback + * description: + * /grade/callback/{outputFilename}: + * post: + * summary: Not directly called by the user. Tells the API that a container grading job has finished and creates relevant entities from the results. + * tags: + * - Grader + * responses: + * '200': + * description: OK + * parameters: + * - name: outputFilename + * in: path + * required: true + * schema: + * type: string + */ Router.post('/callback/:outputFile', GraderController.tangoCallback) //Unauthorized route so tango can make callback without needing token export default Router \ No newline at end of file diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index a26686ce..2950bb32 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -35,7 +35,6 @@ async function grade(submissionId: number) { let score = 0 let feedback = '' - let allScores = [] //This is the return value, the serializer parses it into a GraderInfo object for the controller to return let containerGrading = true //Is set to false if no container autograders are found for this assignment //Run Non-Container Autograders @@ -54,7 +53,7 @@ async function grade(submissionId: number) { score: problemScore, feedback: problemFeedback } - allScores.push(await submissionProblemScoreService.create(problemScoreObj)) + submissionProblemScoreService.create(problemScoreObj) } } @@ -69,7 +68,7 @@ async function grade(submissionId: number) { }) initializeMinio(bucketName) - var response = null + var jobResponse = null const labName = `${bucketName}-${submission.assignmentId}` const optionFiles = [] const openResponse = await createCourse(labName) @@ -95,7 +94,7 @@ async function grade(submissionId: number) { timeout: timeout, callback_url: `http://api:3001/grade/callback/${labName}-${submissionId}-output.txt` } - response = await addJob(labName, jobOptions) + jobResponse = await addJob(labName, jobOptions) } } catch (e: any) { throw new Error(e) @@ -108,12 +107,14 @@ async function grade(submissionId: number) { score: score, //Sum of all SubmissionProblemScore scores feedback: feedback //Concatination of SubmissionProblemScore feedbacks } - allScores.push(await submissionScoreService.create(scoreObj)) + submissionScoreService.create(scoreObj) //If containergrading is true, tangoCallback handles assignmentScore creation - if (containerGrading === false) updateAssignmentScore(submission, score) - - return response + if (containerGrading === false) { + updateAssignmentScore(submission, score) + return {message: `Noncontainer autograding completed successfully`, submissionScore: scoreObj} + } + return {message: `Autograder successfully added job #${jobResponse?.jobId} to the queue with status message ${jobResponse?.statusMsg}`} } From ec222e7d17c7d6d9c534a66c0d1a8212afdc8323 Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Fri, 26 Apr 2024 20:43:24 -0400 Subject: [PATCH 16/16] Pulled grading function's ncag and cag sections into separate functions to improve clarity --- .../src/entities/grader/grader.service.ts | 80 +++++++++++-------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts index 2950bb32..e3786e8c 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -8,16 +8,15 @@ import assignmentScoreService from '../assignmentScore/assignmentScore.service' import courseService from '../course/course.service' import { addJob, createCourse, uploadFile, pollJob } from '../../tango/tango.service' -import { SubmissionScore, SubmissionProblemScore, AssignmentScore, Submission } from 'devu-shared-modules' +import { SubmissionScore, SubmissionProblemScore, AssignmentScore, Submission, NonContainerAutoGrader, AssignmentProblem } from 'devu-shared-modules' import { checkAnswer } from '../nonContainerAutoGrader/nonContainerAutoGrader.grader' import { serialize as serializeNonContainer } from '../nonContainerAutoGrader/nonContainerAutoGrader.serializer' import { serialize as serializeAssignmentScore } from '../assignmentScore/assignmentScore.serializer' import { serialize as serializeSubmissionScore} from '../submissionScore/submissionScore.serializer' import { serialize as serializeSubmission } from '../submission/submission.serializer' +import { serialize as serializeAssignmentProblem } from '../assignmentProblem/assignmentProblem.serializer' import { downloadFile, initializeMinio } from '../../fileStorage' -import environment from '../../environment' - async function grade(submissionId: number) { const submissionModel = await submissionService.retrieve(submissionId) if (!submissionModel) throw new Error('Submission not found.') @@ -29,35 +28,67 @@ async function grade(submissionId: number) { const form = content.form const filepaths: string[] = content.filepaths - const nonContainerAutograders = await nonContainerAutograderService.listByAssignmentId(assignmentId) - const assignmentProblems = await assignmentProblemService.list(assignmentId) + const nonContainerAutograders = (await nonContainerAutograderService.listByAssignmentId(assignmentId)).map(model => serializeNonContainer(model)) + const assignmentProblems = (await assignmentProblemService.list(assignmentId)).map(model => serializeAssignmentProblem(model)) - let score = 0 let feedback = '' - let containerGrading = true //Is set to false if no container autograders are found for this assignment //Run Non-Container Autograders + const ncagResults = await runNonContainerAutograders(form, nonContainerAutograders, assignmentProblems, submissionId) + score += ncagResults.score + feedback += ncagResults.feedback + + //Run Container Autograders + const cagResults = await runContainerAutograders(filepaths, submission, assignmentId) + const jobResponse = cagResults.jobResponse + const containerGrading = cagResults.containerGrading + + //Grading is finished. Create SubmissionScore and AssignmentScore and save to db. + const scoreObj: SubmissionScore = { + submissionId: submissionId, + score: score, //Sum of all SubmissionProblemScore scores + feedback: feedback //Concatination of SubmissionProblemScore feedbacks + } + submissionScoreService.create(scoreObj) + + //If containergrading is true, tangoCallback handles assignmentScore creation + if (containerGrading === false) { + updateAssignmentScore(submission, score) + return {message: `Noncontainer autograding completed successfully`, submissionScore: scoreObj} + } + return {message: `Autograder successfully added job #${jobResponse?.jobId} to the queue with status message ${jobResponse?.statusMsg}`} +} + +async function runNonContainerAutograders(form: any, nonContainerAutograders: NonContainerAutoGrader[], assignmentProblems: AssignmentProblem[], submissionId: number) { + let score = 0 + let feedback = '' + for (const question in form) { const nonContainerGrader = nonContainerAutograders.find(grader => grader.question === question) const assignmentProblem = assignmentProblems.find(problem => problem.problemName === question) if (nonContainerGrader && assignmentProblem) { - const [problemScore, problemFeedback] = checkAnswer(form[question], serializeNonContainer(nonContainerGrader)) + const [problemScore, problemFeedback] = checkAnswer(form[question], nonContainerGrader) score += problemScore - feedback += problemFeedback + '\n' + feedback += `${problemFeedback}\n` const problemScoreObj: SubmissionProblemScore = { submissionId: submissionId, - assignmentProblemId: assignmentProblem.id, + assignmentProblemId: assignmentProblem.id ?? 0, //This should never be undefined, not sure why our types have id set as optional score: problemScore, feedback: problemFeedback } submissionProblemScoreService.create(problemScoreObj) } } + return {score, feedback} +} + +async function runContainerAutograders(filepaths: string[], submission: Submission, assignmentId: number) { + let containerGrading = true + let jobResponse = null - //Run Container Autograders const {graderData, makefileData, autogradingImage, timeout} = await containerAutograderService.getGraderByAssignmentId(assignmentId) if (!graderData || !makefileData || !autogradingImage || !timeout) { containerGrading = false @@ -67,8 +98,7 @@ async function grade(submissionId: number) { return course ? (course.number + course.semester + course.id).toLowerCase() : 'submission' }) initializeMinio(bucketName) - - var jobResponse = null + const labName = `${bucketName}-${submission.assignmentId}` const optionFiles = [] const openResponse = await createCourse(labName) @@ -82,17 +112,15 @@ async function grade(submissionId: number) { optionFiles.push({localFile: filepath, destFile: filepath}) } } - console.log(environment.apiUrl) - console.log(labName) const jobOptions = { image: autogradingImage, files: [{localFile: "Graderfile", destFile: "autograde.tar"}, {localFile: "Makefile", destFile: "Makefile"},] .concat(optionFiles), - jobName: `${labName}-${submissionId}`, - output_file: `${labName}-${submissionId}-output.txt`, + jobName: `${labName}-${submission.id}`, + output_file: `${labName}-${submission.id}-output.txt`, timeout: timeout, - callback_url: `http://api:3001/grade/callback/${labName}-${submissionId}-output.txt` + callback_url: `http://api:3001/grade/callback/${labName}-${submission.id}-output.txt` } jobResponse = await addJob(labName, jobOptions) } @@ -100,21 +128,7 @@ async function grade(submissionId: number) { throw new Error(e) } } - - //Grading is finished. Create SubmissionScore and AssignmentScore and save to db. - const scoreObj: SubmissionScore = { - submissionId: submissionId, - score: score, //Sum of all SubmissionProblemScore scores - feedback: feedback //Concatination of SubmissionProblemScore feedbacks - } - submissionScoreService.create(scoreObj) - - //If containergrading is true, tangoCallback handles assignmentScore creation - if (containerGrading === false) { - updateAssignmentScore(submission, score) - return {message: `Noncontainer autograding completed successfully`, submissionScore: scoreObj} - } - return {message: `Autograder successfully added job #${jobResponse?.jobId} to the queue with status message ${jobResponse?.statusMsg}`} + return {containerGrading, jobResponse} }