From e3c9dd5a683c56a278ebbc96dc853836a94e6d8b Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Wed, 21 Feb 2024 16:33:15 -0500 Subject: [PATCH 1/2] Rebased from develop, fixed swagger paths --- devU-api/scripts/populate-db.ts | 42 +++++----- .../assignmentScore/assignmentScore.model.ts | 2 +- .../src/entities/category/category.model.ts | 2 +- .../containerAutoGrader.model.ts | 20 +++++ .../containerAutoGrader.router.ts | 77 +++++++++++++++++++ .../entities/courseScore/courseScore.model.ts | 2 +- .../src/entities/grader/grader.controller.ts | 23 ++++++ devU-api/src/entities/grader/grader.router.ts | 35 +++++++++ .../src/entities/grader/grader.serializer.ts | 9 +++ .../src/entities/grader/grader.service.ts | 51 ++++++++++++ .../grader/tests/grader.controller.test.ts | 76 ++++++++++++++++++ .../grader/tests/grader.serializer.test.ts | 57 ++++++++++++++ .../nonContainerAutoGrader.grader.ts | 3 +- .../nonContainerAutoGrader.model.ts | 21 +++++ .../nonContainerAutoGrader.router.ts | 64 +++++++++++++++ devU-api/src/entities/user/user.router.ts | 2 +- devU-api/src/router/index.ts | 2 + devU-api/src/utils/swagger.utils.ts | 5 +- devu-shared/src/index.ts | 1 + devu-shared/src/types/grader.types.ts | 8 ++ 20 files changed, 475 insertions(+), 27 deletions(-) create mode 100644 devU-api/src/entities/grader/grader.controller.ts create mode 100644 devU-api/src/entities/grader/grader.router.ts create mode 100644 devU-api/src/entities/grader/grader.serializer.ts create mode 100644 devU-api/src/entities/grader/grader.service.ts create mode 100644 devU-api/src/entities/grader/tests/grader.controller.test.ts create mode 100644 devU-api/src/entities/grader/tests/grader.serializer.test.ts create mode 100644 devu-shared/src/types/grader.types.ts diff --git a/devU-api/scripts/populate-db.ts b/devU-api/scripts/populate-db.ts index 966b49f0..b1712049 100644 --- a/devU-api/scripts/populate-db.ts +++ b/devU-api/scripts/populate-db.ts @@ -32,11 +32,9 @@ async function SendPOST(path: string, requestBody: string) { /* -- Billy -- Student in 302 - has submitted two assignments, one is graded -- Bob -- Student in 312 - has submitted two assignments, one is graded -- Jones -- Student in 302 & 312 @@ -112,11 +110,20 @@ async function RunRequests() { //AssignmentProblems - const assign312_quiz_q1 = await SendPOST("/assignment-problems", JSON.stringify({ - assignmentId: assign312_quiz, problemName: "q1", maxScore: 5 + SendPOST("/assignment-problems", JSON.stringify({ + assignmentId: assign312_quiz, problemName: "Of the following letters A-D, which is B?", maxScore: 5 })) - const assign312_quiz_q2 = await SendPOST("/assignment-problems", JSON.stringify({ - assignmentId: assign312_quiz, problemName: "q2", maxScore: 5 + SendPOST("/assignment-problems", JSON.stringify({ + assignmentId: assign312_quiz, problemName: "Of the following letters A-D, which is C?", maxScore: 5 + })) + + + //NonContainerAutoGraders + SendPOST("/nonContainerAutoGrader", JSON.stringify({ + assignmentId: assign312_quiz, question: "Of the following letters A-D, which is B?", score: 5, correctString: "B" + })) + SendPOST("/nonContainerAutoGrader", JSON.stringify({ + assignmentId: assign312_quiz, question: "Of the following letters A-D, which is C?", score: 5, correctString: "C" })) @@ -138,17 +145,14 @@ async function RunRequests() { submitterIp: "127.0.0.1", submittedBy: userBob })) const submission_bob_312_quiz1 = await SendPOST("/submissions", JSON.stringify({ - courseId: course312, assignmentId: assign312_quiz, userId: userBob, content: "B, D", type: "text", submitterIp: "127.0.0.1", submittedBy: userBob - })) - + courseId: course312, assignmentId: assign312_quiz, userId: userBob, + content: '{"form":{"Of the following letters A-D, which is B?":"B","Of the following letters A-D, which is C?":"D"},"filepaths":""}', + type: "json", submitterIp: "127.0.0.1", submittedBy: userBob + })) - //SubmissionProblemScores - SendPOST("/submission-problem-scores", JSON.stringify({ - submissionId: submission_bob_312_quiz1, assignmentProblemId: assign312_quiz_q1, score: 5, feedback: "Good job!", releasedAt: "2024-02-09T17:00:00-0500" - })) - SendPOST("/submission-problem-scores", JSON.stringify({ - submissionId: submission_bob_312_quiz1, assignmentProblemId: assign312_quiz_q2, score: 0, feedback: "Incorrect, the correct answer was C.", releasedAt: "2024-02-09T17:00:00-0500" - })) + + //Grading (creates a SubmissionScore and SubmissionProblemScores) + await SendPOST("/grade/" + submission_bob_312_quiz1, JSON.stringify({})) //SubmissionScores @@ -158,9 +162,6 @@ async function RunRequests() { SendPOST("/submission-scores", JSON.stringify({ submissionId: submission_bob_312_1, score: 20, feedback: "no", releasedAt: "2024-03-02T18:34:57-0500" })) - SendPOST("/submission-scores", JSON.stringify({ - submissionId: submission_bob_312_1, score: 5, feedback: "1/2 Questions Correct", releasedAt: "2024-02-09T17:00:00-0500" - })) //AssignmentScore - ROUTE NOT FUNCTIONAL @@ -187,5 +188,4 @@ async function RunRequests() { } -RunRequests() - +RunRequests() \ No newline at end of file diff --git a/devU-api/src/entities/assignmentScore/assignmentScore.model.ts b/devU-api/src/entities/assignmentScore/assignmentScore.model.ts index 6574f9d6..cbfeda9b 100644 --- a/devU-api/src/entities/assignmentScore/assignmentScore.model.ts +++ b/devU-api/src/entities/assignmentScore/assignmentScore.model.ts @@ -18,7 +18,7 @@ export default class AssignmentScoreModel { * @swagger * tags: * - name: AssignmentScore - * description: Route is currently non-functional, TS2305 error (Issue #34) + * description: Route is currently non-functional, TS2305 error * components: * schemas: * AssignmentScore: diff --git a/devU-api/src/entities/category/category.model.ts b/devU-api/src/entities/category/category.model.ts index 118d3ad6..a3cd797f 100644 --- a/devU-api/src/entities/category/category.model.ts +++ b/devU-api/src/entities/category/category.model.ts @@ -15,7 +15,7 @@ export default class CategoryModel { * @swagger * tags: * - name: Categories - * description: Route is currently non-functional, TS2305 error (Issue #34) + * description: Route is currently non-functional, TS2305 error * components: * schemas: * Category: diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts index 03286af4..2ce9cb5c 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts @@ -13,6 +13,26 @@ import AssignmentModel from '../assignment/assignment.model' @Entity('container_auto_grader') export default class ContainerAutoGraderModel { + /** + * @swagger + * tags: + * - name: ContainerAutoGraders + * description: + * components: + * schemas: + * ContainerAutoGrader: + * type: object + * required: [assignmentId, autoGradingImage, timeout] + * properties: + * assignmentId: + * type: integer + * autoGradingImage: + * type: string + * timeout: + * type: integer + * description: Must be a positive integer + */ + @PrimaryGeneratedColumn() id: number diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts index eee17503..6c7b5410 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts @@ -14,6 +14,11 @@ const upload = multer(); * /container-auto-graders: * get: * summary: Retrieve a list of all container auto graders + * tags: + * - ContainerAutoGraders + * responses: + * '200': + * description: OK */ Router.get('/', ContainerAutoGraderController.get); @@ -22,6 +27,17 @@ Router.get('/', ContainerAutoGraderController.get); * /container-auto-graders/{id}: * get: * summary: Retrieve a single container auto grader + * tags: + * - ContainerAutoGraders + * responses: + * '200': + * description: OK + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer */ Router.get('/:id', asInt(), ContainerAutoGraderController.detail); @@ -30,6 +46,28 @@ Router.get('/:id', asInt(), ContainerAutoGraderController.detail); * /container-auto-graders: * post: * summary: Create a new container auto grader + * tags: + * - ContainerAutoGraders + * responses: + * '200': + * description: OK + * requestBody: + * content: + * application/x-www-form-urlencoded: + * schema: + * $ref: '#/components/schemas/ContainerAutoGrader' + * multipart/form-data: + * schema: + * type: object + * properties: + * makefileFile: + * type: string + * format: binary + * description: Swagger file upload currently not set up properly, expect an error + * graderFile: + * type: string + * format: binary + * description: Swagger file upload currently not set up properly, expect an error */ Router.post('/', upload.fields([{name: 'graderFile'},{name: 'makefileFile'}]), validator, ContainerAutoGraderController.post); @@ -38,6 +76,34 @@ Router.post('/', upload.fields([{name: 'graderFile'},{name: 'makefileFile'}]), v * /container-auto-graders/{id}: * put: * summary: Update a container auto grader's grader file and/or makefile + * tags: + * - ContainerAutoGraders + * responses: + * '200': + * description: OK + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer + * requestBody: + * content: + * application/x-www-form-urlencoded: + * schema: + * $ref: '#/components/schemas/ContainerAutoGrader' + * multipart/form-data: + * schema: + * type: object + * properties: + * makefileFile: + * type: string + * format: binary + * description: Swagger file upload currently not set up properly, expect an error + * graderFile: + * type: string + * format: binary + * description: Swagger file upload currently not set up properly, expect an error */ Router.put('/:id', asInt(), upload.fields([{name: 'graderFile'},{name: 'makefileFile'}]), validator, ContainerAutoGraderController.put); @@ -46,6 +112,17 @@ Router.put('/:id', asInt(), upload.fields([{name: 'graderFile'},{name: 'makefile * /container-auto-graders/{id}: * delete: * summary: Delete a container auto grader + * tags: + * - ContainerAutoGraders + * responses: + * '200': + * description: OK + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer */ Router.delete('/:id', asInt(), ContainerAutoGraderController._delete); diff --git a/devU-api/src/entities/courseScore/courseScore.model.ts b/devU-api/src/entities/courseScore/courseScore.model.ts index 98b48d91..b66d87c1 100644 --- a/devU-api/src/entities/courseScore/courseScore.model.ts +++ b/devU-api/src/entities/courseScore/courseScore.model.ts @@ -9,7 +9,7 @@ export default class CourseScoreModel { * @swagger * tags: * - name: CourseScores - * description: Route is currently non-functional, TS2305 error (Issue #34) + * description: Route is currently non-functional, TS2305 error * components: * schemas: * CourseScore: diff --git a/devU-api/src/entities/grader/grader.controller.ts b/devU-api/src/entities/grader/grader.controller.ts new file mode 100644 index 00000000..bd766ca0 --- /dev/null +++ b/devU-api/src/entities/grader/grader.controller.ts @@ -0,0 +1,23 @@ +import { Request, Response, NextFunction } from 'express' + +import GraderService from './grader.service' + +import { GenericResponse, NotFound } from '../../utils/apiResponse.utils' + +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 = serialize(grade) + + res.status(200).json(response) + } catch (err) { + res.status(400).json(new GenericResponse(err.message)) + } +} + +export default { grade } diff --git a/devU-api/src/entities/grader/grader.router.ts b/devU-api/src/entities/grader/grader.router.ts new file mode 100644 index 00000000..1ed309a8 --- /dev/null +++ b/devU-api/src/entities/grader/grader.router.ts @@ -0,0 +1,35 @@ +// Libraries +import express from 'express' + +// Middleware +//import validator from './grader.validator' +import { asInt } from '../../middleware/validator/generic.validator' + +// Controller +import GraderController from './grader.controller' + +const Router = express.Router() + +/** + * @swagger + * tags: + * - name: Grader + * description: + * /grade/{id}: + * post: + * summary: Grade a submission, currently only with non-container autograders + * tags: + * - Grader + * responses: + * '200': + * description: OK + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer + */ +Router.post('/:id', asInt(), GraderController.grade) + +export default Router \ No newline at end of file diff --git a/devU-api/src/entities/grader/grader.serializer.ts b/devU-api/src/entities/grader/grader.serializer.ts new file mode 100644 index 00000000..235a797e --- /dev/null +++ b/devU-api/src/entities/grader/grader.serializer.ts @@ -0,0 +1,9 @@ +import { SubmissionScore, SubmissionProblemScore, GraderInfo } from 'devu-shared-modules' + +export function serialize(scores: any[]): GraderInfo { + const submissionScore = scores.pop() + return { + submissionScore: submissionScore as SubmissionScore, + submissionProblemScores: scores as SubmissionProblemScore[] + } +} diff --git a/devU-api/src/entities/grader/grader.service.ts b/devU-api/src/entities/grader/grader.service.ts new file mode 100644 index 00000000..16458665 --- /dev/null +++ b/devU-api/src/entities/grader/grader.service.ts @@ -0,0 +1,51 @@ +import submissionService from '../submission/submission.service' +import submissionScoreService from '../submissionScore/submissionScore.service' +import submissionProblemScoreService from '../submissionProblemScore/submissionProblemScore.service' +import nonContainerAutoGraderService from '../nonContainerAutoGrader/nonContainerAutoGrader.service' +import assignmentProblemService from '../assignmentProblem/assignmentProblem.service' + +import { SubmissionScore, SubmissionProblemScore } from 'devu-shared-modules' +import { checkAnswer } from '../nonContainerAutoGrader/nonContainerAutoGrader.grader' + +export async function grade(id: number) { + const submission = await submissionService.retrieve(id) + if (!submission) return null + + const assignmentId = submission.assignmentId + + const content = JSON.parse(submission.content) + const form = content.form + + const nonContainerAutograders = await nonContainerAutoGraderService.listByAssignmentId(assignmentId) + const assignmentProblems = await assignmentProblemService.list(assignmentId) + + + var score = 0 + var allScores = new Array() //This is the return value, the serializer parses it into a GraderInfo object for the controller to return + for (const question in form) { + const grader = nonContainerAutograders.find(grader => grader.question === question) + const assignmentProblem = assignmentProblems.find(problem => problem.problemName === question) + + if (grader && assignmentProblem) { + const problemScore = (await checkAnswer(form[question], grader)) ?? 0 + score += problemScore + + const problemScoreObj: SubmissionProblemScore = { + submissionId: id, + assignmentProblemId: assignmentProblem.id, + score: problemScore, + feedback: '' //grader doesn't currently return feedback + } + allScores.push(await submissionProblemScoreService.create(problemScoreObj)) + } + } + const scoreObj: SubmissionScore = { + submissionId: id, + score: score, + feedback: '' //graders currently don't return feedback, will be the concatination of SubmissionProblemScore feedbacks + } + allScores.push(await submissionScoreService.create(scoreObj)) + return allScores +} + +export default { grade } \ No newline at end of file diff --git a/devU-api/src/entities/grader/tests/grader.controller.test.ts b/devU-api/src/entities/grader/tests/grader.controller.test.ts new file mode 100644 index 00000000..0c317991 --- /dev/null +++ b/devU-api/src/entities/grader/tests/grader.controller.test.ts @@ -0,0 +1,76 @@ +import { GraderInfo, SubmissionScore, SubmissionProblemScore } from '../../../devu-shared-modules' +import SubmissionModel from '../../submission/submission.model' + +import controller from '../grader.controller' + +import GraderService from '../grader.service' + +import { serialize } from '../grader.serializer' + +import Testing from '../../utils/testing.utils' + +import { GenericResponse } from '../../utils/apiResponse.utils' + +//THIS TEST FAILS, + +// Testing Globals +let req: any +let res: any +let next: any + +let mockedSubmission: SubmissionModel +let expectedReturn = new Array() +let expectedResult: GraderInfo +let expectedError: Error + +let expectedScore: SubmissionScore +let expectedProblemScore1: SubmissionProblemScore +let expectedProblemScore2: SubmissionProblemScore + + +describe('GraderController', () => { + beforeEach(() => { + req = Testing.fakeRequest() + res = Testing.fakeResponse() + next = Testing.fakeNext() + + mockedSubmission = Testing.generateTypeOrm(SubmissionModel) + + expectedReturn.push(expectedProblemScore1) + expectedReturn.push(expectedProblemScore2) + expectedReturn.push(expectedScore) + expectedResult = serialize(expectedReturn) + + expectedError = new Error('Expected Error') + }) + describe('POST - /grade/:id', () => { + describe('200 - Ok', () => { + beforeEach(async () => { + GraderService.grade = jest.fn().mockImplementation(() => Promise.resolve(mockedSubmission)) + await controller.grade(req, res, next) + }) + test('Status code is 200', () => expect(res.status).toBeCalledWith(200)) + test('Returns Grading Info', () => expect(res.json).toBeCalledWith(expectedResult)) + }) + describe('400 - Bad Request', () => { + beforeEach(async () => { + GraderService.grade = jest.fn().mockImplementation(() => Promise.reject(expectedError)) + + try { + await controller.grade(req, res, next) + + fail('Expected test to throw') + } catch { + // continue to tests + } + }) + + test('Status code is 400', () => expect(res.status).toBeCalledWith(400)) + test('Responds with generic error', () => + expect(res.json).toBeCalledWith(new GenericResponse(expectedError.message))) + test('Next not called', () => expect(next).toBeCalledTimes(0)) + }) + }) + + +}) \ No newline at end of file diff --git a/devU-api/src/entities/grader/tests/grader.serializer.test.ts b/devU-api/src/entities/grader/tests/grader.serializer.test.ts new file mode 100644 index 00000000..5d3c9b28 --- /dev/null +++ b/devU-api/src/entities/grader/tests/grader.serializer.test.ts @@ -0,0 +1,57 @@ +import { serialize } from '../grader.serializer' + +import SubmissionScoreModel from '../../submissionScore/submissionScore.model' +import SubmissionProblemScoreModel from '../../submissionProblemScore/submissionProblemScore.model' + +import Testing from '../../utils/testing.utils' + +let mockSubmissionScore: SubmissionScoreModel +let mockSubmissionProblemScore1: SubmissionProblemScoreModel +let mockSubmissionProblemScore2: SubmissionProblemScoreModel +let mockArray = new Array() + +describe('Grader Serializer', () => { + beforeEach(() => { + mockSubmissionScore = Testing.generateTypeOrm(SubmissionScoreModel) + mockSubmissionProblemScore1 = Testing.generateTypeOrm(SubmissionProblemScoreModel) + mockSubmissionProblemScore2 = Testing.generateTypeOrm(SubmissionProblemScoreModel) + + mockSubmissionScore.id = 1 + mockSubmissionScore.submissionId = 4 + mockSubmissionScore.score = 5 + mockSubmissionScore.feedback = "q1: 5/5, q2: 0/5" + mockSubmissionScore.createdAt = new Date + mockSubmissionScore.updatedAt = new Date + + mockSubmissionProblemScore1.id = 1 + mockSubmissionProblemScore1.submissionId = 4 + mockSubmissionProblemScore1.assignmentProblemId = 1 + mockSubmissionProblemScore1.score = 5 + mockSubmissionProblemScore1.feedback = "Correct, 5/5 points" + mockSubmissionProblemScore1.createdAt = new Date + mockSubmissionProblemScore1.updatedAt = new Date + + mockSubmissionProblemScore2.id = 2 + mockSubmissionProblemScore2.submissionId = 4 + mockSubmissionProblemScore2.assignmentProblemId = 2 + mockSubmissionProblemScore2.score = 0 + mockSubmissionProblemScore2.feedback = "Incorrect, 0/5 points" + mockSubmissionProblemScore2.createdAt = new Date + mockSubmissionProblemScore2.updatedAt = new Date + + mockArray.push(mockSubmissionProblemScore1) + mockArray.push(mockSubmissionProblemScore2) + mockArray.push(mockSubmissionScore) + }) + + describe('Serializing grade info', () => { + test('GraderInfo values exist in the response', () => { + const expectedResult = serialize(mockArray) + + expect(expectedResult).toBeDefined() + expect(expectedResult.submissionScore).toEqual(mockSubmissionScore) + expect(expectedResult.submissionProblemScores[0]).toEqual(mockSubmissionProblemScore1) + expect(expectedResult.submissionProblemScores[1]).toEqual(mockSubmissionProblemScore2) + }) + }) +}) \ No newline at end of file diff --git a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts index 4389589a..60aa62c9 100644 --- a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts +++ b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts @@ -1,6 +1,7 @@ import { NonContainerAutoGrader } from 'devu-shared-modules' +import NonContainerAutoGraderModel from './nonContainerAutoGrader.model' -export async function checkAnswer(studentAnswer: string, nonContainerAutoGrader: NonContainerAutoGrader) { +export async function checkAnswer(studentAnswer: string, nonContainerAutoGrader: NonContainerAutoGrader | NonContainerAutoGraderModel) { if (studentAnswer === nonContainerAutoGrader.correctString) { return nonContainerAutoGrader.score } diff --git a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.model.ts b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.model.ts index 160de1ff..fce6e727 100644 --- a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.model.ts +++ b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.model.ts @@ -12,6 +12,27 @@ import AssignmentModel from '../assignment/assignment.model' @Entity({ name: 'nonContainerAutoGrader' }) export default class NonContainerAutoGraderModel { + /** + * @swagger + * tags: + * - name: NonContainerAutoGraders + * description: + * components: + * schemas: + * NonContainerAutoGrader: + * type: object + * required: [assignmentId, question, score, correctString] + * properties: + * assignmentId: + * type: integer + * question: + * type: string + * score: + * type: number + * correctString: + * type: string + */ + @PrimaryGeneratedColumn() id: number diff --git a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.router.ts b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.router.ts index 81b9ade0..6ce0e103 100644 --- a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.router.ts +++ b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.router.ts @@ -15,6 +15,11 @@ const Router = express.Router() * /nonContainerAutoGrader: * get: * summary: Retrieve a list of all nonContainerAutoGraders + * tags: + * - NonContainerAutoGraders + * responses: + * '200': + * description: OK */ Router.get('/', nonContainerQuestions.get) @@ -23,6 +28,17 @@ Router.get('/', nonContainerQuestions.get) * /nonContainerAutoGrader/byAssignmentId/{assignmentId}: * get: * summary: Retrieve a list of all nonContainerAutoGrader with the assignment ID + * tags: + * - NonContainerAutoGraders + * responses: + * '200': + * description: OK + * parameters: + * - name: assignmentId + * in: path + * required: true + * schema: + * type: integer */ Router.get('/byAssignmentId/:assignmentId', asInt("assignmentId"), nonContainerQuestions.getByAssignmentId) @@ -31,6 +47,17 @@ Router.get('/byAssignmentId/:assignmentId', asInt("assignmentId"), nonContainerQ * /nonContainerAutoGrader/byId/{id}: * get: * summary: Retrieve a single question + * tags: + * - NonContainerAutoGraders + * responses: + * '200': + * description: OK + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer */ Router.get('/byId/:id', asInt(), nonContainerQuestions.detail) @@ -39,6 +66,16 @@ Router.get('/byId/:id', asInt(), nonContainerQuestions.detail) * /nonContainerAutoGrader: * post: * summary: Create a question + * tags: + * - NonContainerAutoGraders + * responses: + * '200': + * description: OK + * requestBody: + * content: + * application/x-www-form-urlencoded: + * schema: + * $ref: '#/components/schemas/NonContainerAutoGrader' */ Router.post('/', validator, nonContainerQuestions.post) @@ -47,6 +84,22 @@ Router.post('/', validator, nonContainerQuestions.post) * /nonContainerAutoGrader: * put: * summary: Update a question + * tags: + * - NonContainerAutoGraders + * responses: + * '200': + * description: OK + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer + * requestBody: + * content: + * application/x-www-form-urlencoded: + * schema: + * $ref: '#/components/schemas/NonContainerAutoGrader' */ Router.put('/:id', asInt(), validator, nonContainerQuestions.put) @@ -55,6 +108,17 @@ Router.put('/:id', asInt(), validator, nonContainerQuestions.put) * /nonContainerAutoGrader/{id}: * delete: * summary: Delete a question + * tags: + * - NonContainerAutoGraders + * responses: + * '200': + * description: OK + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer */ Router.delete('/:id', asInt(), nonContainerQuestions._delete) diff --git a/devU-api/src/entities/user/user.router.ts b/devU-api/src/entities/user/user.router.ts index b8ef1d1a..7d8d5428 100644 --- a/devU-api/src/entities/user/user.router.ts +++ b/devU-api/src/entities/user/user.router.ts @@ -61,7 +61,7 @@ Router.post('/', validator, UserController.post) * @swagger * /users/{id}: * put: - * summary: Update a user + * summary: Update a user, all fields required but only preferredName is updated * tags: * - Users * responses: diff --git a/devU-api/src/router/index.ts b/devU-api/src/router/index.ts index 36e81900..9cb212df 100644 --- a/devU-api/src/router/index.ts +++ b/devU-api/src/router/index.ts @@ -16,6 +16,7 @@ import containerAutoGrader from '../entities/containerAutoGrader/containerAutoGr import assignmentProblem from '../entities/assignmentProblem/assignmentProblem.router' import submissionProblemScore from '../entities/submissionProblemScore/submissionProblemScore.router' import fileUpload from '../fileUpload/fileUpload.router' +import grader from '../entities/grader/grader.router' import { isAuthorized } from '../auth/auth.middleware' @@ -35,6 +36,7 @@ Router.use('/nonContainerAutoGrader', isAuthorized, nonContainerAutoGraderRouter Router.use('/container-auto-graders', isAuthorized, containerAutoGrader) Router.use('/submission-problem-scores', isAuthorized, submissionProblemScore) Router.use('/file-upload', isAuthorized, fileUpload) +Router.use('/grade', isAuthorized, grader) Router.use('/login', login) Router.use('/logout', logout) diff --git a/devU-api/src/utils/swagger.utils.ts b/devU-api/src/utils/swagger.utils.ts index 7168b40a..e8fcf9c5 100644 --- a/devU-api/src/utils/swagger.utils.ts +++ b/devU-api/src/utils/swagger.utils.ts @@ -22,7 +22,10 @@ const swaggerOptioner = { }], }, apis: [ - './src/router/*.ts', './src/*/*.router.ts', './src/*/*.model.ts'], + './src/router/*.ts', + './src/entities/*/*.router.ts', './src/entities/*/*.model.ts', + './src/fileUpload/*.router.ts', './src/fileUpload/*.model.ts', + './src/auth/*/*.router.ts', './src/auth/*/*.model.ts'], } const swaggerSpec = swaggerJSDoc(swaggerOptioner) diff --git a/devu-shared/src/index.ts b/devu-shared/src/index.ts index a89a7b99..3b1187f8 100644 --- a/devu-shared/src/index.ts +++ b/devu-shared/src/index.ts @@ -18,6 +18,7 @@ export * from './types/categoryScore.types' export * from './types/submissionScore.types' export * from './types/fileUpload.types' export * from './types/nonContainerAutoGrader.types' +export * from './types/grader.types' export * from './utils/object.utils' export * from './utils/string.utils' diff --git a/devu-shared/src/types/grader.types.ts b/devu-shared/src/types/grader.types.ts new file mode 100644 index 00000000..6a14f8aa --- /dev/null +++ b/devu-shared/src/types/grader.types.ts @@ -0,0 +1,8 @@ +import { SubmissionProblemScore, SubmissionScore } from ".." + +export type GraderInfo = { + + submissionScore: SubmissionScore + submissionProblemScores: SubmissionProblemScore[] + +} From 987b0ac7639db2729ec72cbf062147950f5d0e15 Mon Sep 17 00:00:00 2001 From: keiferms3 Date: Thu, 22 Feb 2024 20:07:57 -0500 Subject: [PATCH 2/2] Mock container grading implemented. See issue #53 for discussion --- .../containerAutoGrader.model.ts | 10 ++- .../containerAutoGrader.router.ts | 24 -------- .../containerAutoGrader.service.ts | 7 +++ .../src/entities/grader/grader.service.ts | 61 ++++++++++++++++--- .../nonContainerAutoGrader.grader.ts | 5 +- 5 files changed, 70 insertions(+), 37 deletions(-) diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts index 2ce9cb5c..bf66321b 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.model.ts @@ -22,11 +22,17 @@ export default class ContainerAutoGraderModel { * schemas: * ContainerAutoGrader: * type: object - * required: [assignmentId, autoGradingImage, timeout] + * required: [assignmentId, graderFile, autogradingImage, timeout] * properties: * assignmentId: * type: integer - * autoGradingImage: + * 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 diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts index 6c7b5410..398bb016 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.router.ts @@ -56,18 +56,6 @@ Router.get('/:id', asInt(), ContainerAutoGraderController.detail); * application/x-www-form-urlencoded: * schema: * $ref: '#/components/schemas/ContainerAutoGrader' - * multipart/form-data: - * schema: - * type: object - * properties: - * makefileFile: - * type: string - * format: binary - * description: Swagger file upload currently not set up properly, expect an error - * graderFile: - * type: string - * format: binary - * description: Swagger file upload currently not set up properly, expect an error */ Router.post('/', upload.fields([{name: 'graderFile'},{name: 'makefileFile'}]), validator, ContainerAutoGraderController.post); @@ -92,18 +80,6 @@ Router.post('/', upload.fields([{name: 'graderFile'},{name: 'makefileFile'}]), v * application/x-www-form-urlencoded: * schema: * $ref: '#/components/schemas/ContainerAutoGrader' - * multipart/form-data: - * schema: - * type: object - * properties: - * makefileFile: - * type: string - * format: binary - * description: Swagger file upload currently not set up properly, expect an error - * graderFile: - * type: string - * format: binary - * description: Swagger file upload currently not set up properly, expect an error */ Router.put('/:id', asInt(), upload.fields([{name: 'graderFile'},{name: 'makefileFile'}]), validator, ContainerAutoGraderController.put); diff --git a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts index aee2c8d9..fed092bd 100644 --- a/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts +++ b/devU-api/src/entities/containerAutoGrader/containerAutoGrader.service.ts @@ -66,10 +66,17 @@ export async function list() { return await connect().find({ deletedAt: IsNull() }) } +//Currently only used in grader.service.ts, not yet defined in router +export async function listByAssignmentId(assignmentId: number) { + if (!assignmentId) throw new Error('Missing AssignmentId') + return await connect().find({ assignmentId: assignmentId, deletedAt: IsNull() }) +} + export default { create, retrieve, update, _delete, list, + listByAssignmentId, } \ 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 16458665..34eb2632 100644 --- a/devU-api/src/entities/grader/grader.service.ts +++ b/devU-api/src/entities/grader/grader.service.ts @@ -1,11 +1,14 @@ import submissionService from '../submission/submission.service' import submissionScoreService from '../submissionScore/submissionScore.service' import submissionProblemScoreService from '../submissionProblemScore/submissionProblemScore.service' -import nonContainerAutoGraderService from '../nonContainerAutoGrader/nonContainerAutoGrader.service' +import nonContainerAutograderService from '../nonContainerAutoGrader/nonContainerAutoGrader.service' +import containerAutograderService from '../containerAutoGrader/containerAutoGrader.service' import assignmentProblemService from '../assignmentProblem/assignmentProblem.service' -import { SubmissionScore, SubmissionProblemScore } from 'devu-shared-modules' +import { SubmissionScore, SubmissionProblemScore, ContainerAutoGrader } 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' export async function grade(id: number) { const submission = await submissionService.retrieve(id) @@ -15,19 +18,23 @@ export async function grade(id: 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 nonContainerAutograders = await nonContainerAutoGraderService.listByAssignmentId(assignmentId) + const nonContainerAutograders = await nonContainerAutograderService.listByAssignmentId(assignmentId) + const containerAutograders = await containerAutograderService.listByAssignmentId(assignmentId) const assignmentProblems = await assignmentProblemService.list(assignmentId) var score = 0 - var allScores = new Array() //This is the return value, the serializer parses it into a GraderInfo object for the controller to return - for (const question in form) { - const grader = nonContainerAutograders.find(grader => grader.question === question) + var allScores = [] //This is the return value, the serializer parses it into a GraderInfo object for the controller to return + + //Run Non-Container Autograders + for (const question in form) { + const nonContainerGrader = nonContainerAutograders.find(grader => grader.question === question) const assignmentProblem = assignmentProblems.find(problem => problem.problemName === question) - if (grader && assignmentProblem) { - const problemScore = (await checkAnswer(form[question], grader)) ?? 0 + if (nonContainerGrader && assignmentProblem) { + const problemScore = await checkAnswer(form[question], serializeNonContainer(nonContainerGrader)) //Should also return feedback in the future score += problemScore const problemScoreObj: SubmissionProblemScore = { @@ -39,6 +46,28 @@ export async function grade(id: number) { allScores.push(await submissionProblemScoreService.create(problemScoreObj)) } } + + //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: id, + assignmentProblemId: 1, //PLACEHOLDER, an assignmentProblem must exist in the db for this to work + score: result.score, + feedback: result.feedback, + } + allScores.push(await submissionProblemScoreService.create(problemScoreObj)) + score += result.score + } + } + } + const scoreObj: SubmissionScore = { submissionId: id, score: score, @@ -48,4 +77,20 @@ export async function grade(id: number) { return allScores } +//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"}) + + //SubmissionProblemScore 2 + gradeResults.push({score: 5, feedback: "Grader #" + containerAutoGrader.id + " graded " + file + " problem 2 for 5/5 points"}) + + //SubmissionProblemScore 3 + gradeResults.push({score: 10, feedback: "Grader #" + containerAutoGrader.id + " graded " + file + " problem 3 for 10/10 points"}) + + return gradeResults +} + export default { grade } \ No newline at end of file diff --git a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts index 60aa62c9..8816ada2 100644 --- a/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts +++ b/devU-api/src/entities/nonContainerAutoGrader/nonContainerAutoGrader.grader.ts @@ -1,9 +1,8 @@ import { NonContainerAutoGrader } from 'devu-shared-modules' -import NonContainerAutoGraderModel from './nonContainerAutoGrader.model' -export async function checkAnswer(studentAnswer: string, nonContainerAutoGrader: NonContainerAutoGrader | NonContainerAutoGraderModel) { +export async function checkAnswer(studentAnswer: string, nonContainerAutoGrader: NonContainerAutoGrader) { if (studentAnswer === nonContainerAutoGrader.correctString) { return nonContainerAutoGrader.score } - return null + return 0 } \ No newline at end of file