Skip to content

104 grading opts #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions devU-api/src/entities/assignment/assignment.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {

import CourseModel from '../course/course.model'

import { ScoringType } from 'devu-shared-modules'

@Entity('assignments')
export default class AssignmentModel {
/**
Expand Down Expand Up @@ -58,6 +60,11 @@ export default class AssignmentModel {
* type: string
* array: true
* description: filenames of stored attachments, matches the index of the fileHashes, i.e. filename[i] is the name of hash[i]
* scoringType:
* type: string
* enum: [highest-score, latest-submission, no-score]
* default: highest-score
* description: Determines how the final score is chosen for the assignment
*/

@PrimaryGeneratedColumn()
Expand Down Expand Up @@ -109,4 +116,12 @@ export default class AssignmentModel {

@Column({ name: 'attachmentsFilenames', array: true, default: [], type: 'text', nullable: false })
attachmentsFilenames: string[]

@Column({
type: 'enum',
enum: ScoringType,
default: ScoringType.HIGHEST_SCORE,
name: 'scoring_type'
})
scoringType: ScoringType
}
78 changes: 73 additions & 5 deletions devU-api/src/entities/assignment/assignment.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ Router.get('/:assignmentId', asInt('assignmentId'), isAuthorizedByAssignmentStat
* tags:
* - Assignments
* responses:
* '200':
* description: OK
* '201':
* description: Created
* '400':
* description: Bad Request
* parameters:
* - name: courseId
* in: path
Expand All @@ -136,7 +138,41 @@ Router.get('/:assignmentId', asInt('assignmentId'), isAuthorizedByAssignmentStat
* content:
* application/x-www-form-urlencoded:
* schema:
* $ref: '#/components/schemas/Assignment'
* type: object
* required: [courseId, name, categoryName, maxFileSize, disableHandins, startDate, dueDate, endDate]
* properties:
* courseId:
* type: integer
* name:
* type: string
* maxLength: 128
* categoryName:
* type: string
* maxLength: 128
* description:
* type: string
* nullable: true
* maxFileSize:
* type: integer
* maxSubmissions:
* type: integer
* nullable: true
* disableHandins:
* type: boolean
* startDate:
* type: string
* format: date-time
* dueDate:
* type: string
* format: date-time
* endDate:
* type: string
* format: date-time
* scoringType:
* type: string
* enum: [highest-score, latest-submission, no-score]
* default: highest-score
* description: Determines how the final score is chosen for the assignment
*/


Expand All @@ -151,7 +187,9 @@ Router.post('/', isAuthorized('assignmentEditAll'), upload.array('files', 5), va
* - Assignments
* responses:
* '200':
* description: OK
* description: Updated
* '404':
* description: Not Found
* parameters:
* - name: courseId
* in: path
Expand All @@ -168,7 +206,37 @@ Router.post('/', isAuthorized('assignmentEditAll'), upload.array('files', 5), va
* content:
* application/x-www-form-urlencoded:
* schema:
* $ref: '#/components/schemas/Assignment'
* type: object
* properties:
* name:
* type: string
* maxLength: 128
* categoryName:
* type: string
* maxLength: 128
* description:
* type: string
* nullable: true
* maxFileSize:
* type: integer
* maxSubmissions:
* type: integer
* nullable: true
* disableHandins:
* type: boolean
* startDate:
* type: string
* format: date-time
* dueDate:
* type: string
* format: date-time
* endDate:
* type: string
* format: date-time
* scoringType:
* type: string
* enum: [highest-score, latest-submission, no-score]
* description: Determines how the final score is chosen for the assignment
*/
Router.put(
'/:assignmentId',
Expand Down
3 changes: 2 additions & 1 deletion devU-api/src/entities/assignment/assignment.serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export function serialize(assignment: AssignmentModel): Assignment {
disableHandins: assignment.disableHandins,
createdAt: assignment.createdAt.toISOString(),
updatedAt: assignment.updatedAt.toISOString(),
scoringType: assignment.scoringType,
attachmentsHashes: assignment.attachmentsHashes,
attachmentsFilenames: assignment.attachmentsFilenames
attachmentsFilenames: assignment.attachmentsFilenames,
}
}
2 changes: 2 additions & 0 deletions devU-api/src/entities/assignment/assignment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function update(assignment: Assignment) {
disableHandins,
attachmentsHashes,
attachmentsFilenames,
scoringType,
} = assignment

if (!id) throw new Error('Missing Id')
Expand All @@ -44,6 +45,7 @@ export async function update(assignment: Assignment) {
disableHandins,
attachmentsHashes,
attachmentsFilenames,
scoringType,
})
}

Expand Down
6 changes: 6 additions & 0 deletions devU-api/src/entities/assignment/assignment.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { check } from 'express-validator'

import validate from '../../middleware/validator/generic.validator'
import { isBeforeParam, isAfterParam } from '../../middleware/validator/date.validator'
import { ScoringType } from 'devu-shared-modules'

const courseId = check('courseId').isNumeric()
const name = check('name').isString().trim().isLength({ max: 128 })
Expand All @@ -10,6 +11,10 @@ const description = check('description').isString().trim()
const maxFileSize = check('maxFileSize').isNumeric()
const maxSubmissions = check('maxSubmissions').isNumeric().optional({ nullable: true })
const disableHandins = check('disableHandins').isBoolean()
const scoringType = check('scoringType')
.optional()
.isIn(Object.values(ScoringType))
.withMessage(`scoringType must be one of: ${Object.values(ScoringType).join(', ')}`)

const startDate = check('startDate')
.trim()
Expand Down Expand Up @@ -43,6 +48,7 @@ const validator = [
maxFileSize,
maxSubmissions,
disableHandins,
scoringType,
validate,
]

Expand Down
20 changes: 19 additions & 1 deletion devU-api/src/entities/grader/grader.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
AssignmentProblem,
AssignmentScore,
NonContainerAutoGrader,
ScoringType,
Submission,
SubmissionProblemScore,
SubmissionScore,
Expand All @@ -25,6 +26,7 @@ import { downloadFile, initializeMinio } from '../../fileStorage'
import { createNewLab, sendSubmission, waitForJob } from '../../autograders/leviathan.service'
import { DockerFile, LabData, LabFile, SubmissionFile } from 'leviathan-node-sdk'
import path from 'path'
import assignmentService from '../assignment/assignment.service'

async function grade(submissionId: number) {
const submissionModel = await submissionService.retrieve(submissionId)
Expand Down Expand Up @@ -271,11 +273,27 @@ export async function callbackFailure(assignmentId: number, submissionId: number
//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)
const assignment = await assignmentService.retrieve(submission.assignmentId, submission.courseId)

if (assignmentScoreModel) {
//If assignmentScore already exists, update existing entity
const assignmentScore = serializeAssignmentScore(assignmentScoreModel)
assignmentScore.score = score
assignmentScoreService.update(assignmentScore)

// todo use scoring type in assignment entity, leaving this alone for now,
// grader endpoint needs to be refactored
switch (assignment!.scoringType) {
case ScoringType.HIGHEST_SCORE:
break
case ScoringType.LATEST_SUBMISSION:
break
case ScoringType.NO_SCORE:
break
default:
break
}

await assignmentScoreService.update(assignmentScore)
} else {
//Otherwise create a new one
const assignmentScore: AssignmentScore = {
Expand Down
16 changes: 16 additions & 0 deletions devU-api/src/migration/1744570382000-addScoringType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddScoringType1744570382000 implements MigrationInterface {
name = 'AddScoringType1744570382000'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "public"."assignments_scoring_type_enum" AS ENUM('highest-score', 'latest-submission', 'no-score')`);
await queryRunner.query(`ALTER TABLE "assignments" ADD "scoring_type" "public"."assignments_scoring_type_enum" NOT NULL DEFAULT 'highest-score'`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assignments" DROP COLUMN "scoring_type"`);
await queryRunner.query(`DROP TYPE "public"."assignments_scoring_type_enum"`);
}

}
7 changes: 7 additions & 0 deletions devU-shared/src/types/assignment.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export enum ScoringType {
HIGHEST_SCORE = 'highest-score',
LATEST_SUBMISSION = 'latest-submission',
NO_SCORE = 'no-score'
}

export type Assignment = {
id?: number
courseId: number
Expand All @@ -14,5 +20,6 @@ export type Assignment = {
updatedAt?: string
attachmentsHashes ?: string[]
attachmentsFilenames ?: string[]
scoringType?: ScoringType
}