Skip to content
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

Add New Assessment Framework #219

Open
wants to merge 70 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
eb59a20
Add Assessment and Question Independent of Google
Nephelite Sep 27, 2024
bbdf033
Implement Sprint 3 Leftovers
Nephelite Oct 2, 2024
db3e650
Add Assessment Taking UI
Nephelite Oct 4, 2024
4bc8a13
Add Submission and Answer Schema
Nephelite Oct 5, 2024
00f35c1
Add Student Selection Question and Submission Viewing
Nephelite Oct 9, 2024
a1cf009
Fix Submission Edit Permission Bugs
Nephelite Oct 10, 2024
5b584cf
Add Delete Draft Button
Nephelite Oct 10, 2024
13acdff
Merge pull request #226 from NUS-CRISP/staging
dexter-sim Oct 11, 2024
f539019
Implement Student Searching in Student Selection Question
Nephelite Oct 12, 2024
e56e590
Merge pull request #229 from NUS-CRISP/staging
dexter-sim Oct 13, 2024
4e436ed
Merge branch 'NUS-CRISP:main' into main
Nephelite Oct 13, 2024
6edf90e
Merge pull request #231 from NUS-CRISP/staging
dexter-sim Oct 14, 2024
e94f150
Merge branch 'NUS-CRISP:main' into main
Nephelite Oct 14, 2024
9f9d4c0
Add Scoring Element to Questions
Nephelite Oct 17, 2024
b9a982b
Merge pull request #239 from NUS-CRISP/staging
dexter-sim Oct 17, 2024
649996f
Add Submission Score Viewing
Nephelite Oct 21, 2024
ee3df79
Add TA Team Assignment UI
Nephelite Oct 21, 2024
850815e
Add Backend for Custom Team Assignments
Nephelite Oct 22, 2024
17f34c5
Add Result Viewing and Custom Team Assignment
Nephelite Oct 24, 2024
90da3a5
Fix Questions not Saving and Improper AssignmentSet Fetching
Nephelite Oct 24, 2024
8a3c86f
Fix Crashing on NaN
Nephelite Oct 24, 2024
0fc65d7
Add .csv Downloading
Nephelite Oct 24, 2024
2f20a41
Remove gradedBy Field in InternalAssessments
Nephelite Oct 29, 2024
d40446a
Merge branch 'NUS-CRISP:main' into main
Nephelite Oct 30, 2024
268aa32
Add Missing Submission Status For Results
Nephelite Oct 30, 2024
6f8cfff
Fix Improper Hook Usage
Nephelite Oct 31, 2024
9fe027e
Run Prettier
Nephelite Oct 31, 2024
f1c47b7
Add Missing Field to getTableUser
Nephelite Oct 31, 2024
8f94910
Disable EsLint for SubmissionCard
Nephelite Oct 31, 2024
1bb8e76
Add Preliminary Test Files
Nephelite Oct 31, 2024
3f00651
Remove Deprecated Information
Nephelite Oct 31, 2024
c997987
Fix Type Issues with Frontend Build
Nephelite Oct 31, 2024
e2e0a40
Fix Improper Routing of Imports
Nephelite Oct 31, 2024
bd2a531
Fix Compilation Issues and Conflicting Discriminator Keys
Nephelite Nov 1, 2024
7812845
Fix Various Test Cases
Nephelite Nov 2, 2024
cdfab27
Fix Team Related Test Issues
Nephelite Nov 3, 2024
b8401fc
Add Randomizer Exclusion to Grader Assignment
Nephelite Nov 12, 2024
1fdbc59
Fix Submission, Results Viewing Bugs
Nephelite Nov 14, 2024
584bda7
Fix Marking Filters
Nephelite Nov 15, 2024
411f1cd
Ensure Graders can only Submit for Unmarked Assignments
Nephelite Nov 15, 2024
baf400e
Fix Style Issue
Nephelite Nov 16, 2024
e0cb45d
Add Question Type Grouping in Dropdown Selection Based on Auto-Gradin…
Nephelite Nov 17, 2024
cbce2e0
Remove Check Marking Route
Nephelite Nov 21, 2024
857b731
Fix Typo
Nephelite Nov 21, 2024
a6ad022
Fix Internal Assessment Service Test
Nephelite Nov 21, 2024
b706c71
Fixed Incorrect Loading of Team Id in Submission Edit
Nephelite Nov 21, 2024
3e551a2
Fix Submission Test Issues and Remove Async For Loop in Submission Se…
Nephelite Nov 21, 2024
db3f9c0
Merge branch 'staging' into main
Nephelite Nov 21, 2024
738a6d9
Run Prettierrc
Nephelite Nov 21, 2024
316ce76
Merge branch 'main' of https://github.com/Nephelite/CRISP
Nephelite Nov 21, 2024
6e8e7c6
Fix Incorrect Type Cast (string | null Cannot be Cast)
Nephelite Nov 21, 2024
b5030cf
Fix Submission Controller Test File
Nephelite Nov 21, 2024
4a7b913
Run Prettierrc
Nephelite Nov 22, 2024
162e9b8
Break up AssessmentMakeQuestionCard into Smaller Files
Nephelite Nov 23, 2024
283e30d
Merge branch 'staging' into main
Nephelite Nov 23, 2024
70c27bf
Break up TakeAssessmentCard into Smaller Files
Nephelite Nov 23, 2024
032ab25
Adjust Submission Card Display
Nephelite Dec 7, 2024
c3635ae
Add More MRQ Customization
Nephelite Dec 7, 2024
472ef11
Fix Incorrect Wrong Answer Checkbox Display Logic
Nephelite Dec 10, 2024
04ee877
Fix Style Issues (Missing Padding)
Nephelite Dec 10, 2024
3ce4469
Add Check for Grading Completion in Assessment Overview
Nephelite Dec 10, 2024
6364a7a
Add Initial Random Assignments to Unassigned Teams in the TeamSet
Nephelite Dec 10, 2024
328ceb2
Balanced Randomization Function in TA Assignments and Added Validatio…
Nephelite Dec 10, 2024
b39237d
Refactor out TA Assignment Modal
Nephelite Dec 10, 2024
f9ffef9
Fix Bug Involving Missing TeamSet Creating Phantom Assessments
Nephelite Dec 10, 2024
bea7f59
Require Max Marks for Assessments
Nephelite Dec 10, 2024
8f18e91
Add Score Scaling
Nephelite Dec 10, 2024
26b1b22
Fix Formatting
Nephelite Dec 10, 2024
cdbbf22
Fix Test Files
Nephelite Dec 10, 2024
041757a
Remove Console Logs
Nephelite Dec 10, 2024
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
8 changes: 8 additions & 0 deletions backend/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import setupJiraJob from './jobs/jiraJob';
import setupTrofosJob from './jobs/trofosJob';
import accountRoutes from './routes/accountRoutes';
import assessmentRoutes from './routes/assessmentRoutes';
import internalAssessmentRoutes from './routes/internalAssessmentRoutes';
import courseRoutes from './routes/courseRoutes';
import githubRoutes from './routes/githubRoutes';
import jiraRoutes from './routes/jiraRoutes';
Expand All @@ -16,6 +17,9 @@ import teamSetRoutes from './routes/teamSetRoutes';
import userRoutes from './routes/userRoutes';
import codeAnalysisRoutes from './routes/codeAnalysisRoutes';
import { connectToDatabase } from './utils/database';
import submissionRoutes from 'routes/submissionRoutes';
import assessmentAssignmentSetRoutes from 'routes/assessmentAssignmentSetRoutes';
import assessmentResultRoutes from 'routes/assessmentResultRoutes';

const env = process.env.NODE_ENV ?? 'development';
config({ path: `.env.${env}` });
Expand All @@ -42,9 +46,13 @@ app.use('/api/accounts', accountRoutes);
app.use('/api/teams', teamRoutes);
app.use('/api/teamsets', teamSetRoutes);
app.use('/api/assessments', assessmentRoutes);
app.use('/api/internal-assessments', internalAssessmentRoutes);
app.use('/api/submissions', submissionRoutes);
app.use('/api/jira', jiraRoutes);
app.use('/api/metrics', metricRoutes);
app.use('/api/user', userRoutes);
app.use('/api/assessment-results', assessmentResultRoutes);
app.use('/api/assignment-sets', assessmentAssignmentSetRoutes);
app.use('/api/codeanalysis', codeAnalysisRoutes);

app.listen(port, () => {
Expand Down
156 changes: 156 additions & 0 deletions backend/controllers/assessmentAssignmentSetController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// controllers/assessmentAssignmentSetController.ts

import { Request, Response } from 'express';
import {
createAssignmentSet,
getAssignmentSetByAssessmentId,
updateAssignmentSet,
getAssignmentsByTAId,
getUnmarkedAssignmentsByTAId,
} from '../services/assessmentAssignmentSetService';
import { BadRequestError, NotFoundError } from '../services/errors';
import { getAccountId } from '../utils/auth';
import { getUserIdByAccountId } from '../services/accountService';

/**
* Controller to create a new AssessmentAssignmentSet.
*/
export const createAssignmentSetController = async (
req: Request,
res: Response
) => {
const { assessmentId } = req.params;
const { originalTeamSetId } = req.body;

// Validate input
if (!originalTeamSetId) {
return res.status(400).json({ error: 'originalTeamSetId is required' });
}

try {
const assignmentSet = await createAssignmentSet(
assessmentId,
originalTeamSetId
);
res.status(201).json(assignmentSet);
} catch (error) {
if (error instanceof BadRequestError || error instanceof NotFoundError) {
res.status(400).json({ error: error.message });
} else {
console.error('Error creating AssessmentAssignmentSet:', error);
res
.status(500)
.json({ error: 'Failed to create AssessmentAssignmentSet' });
}
}
};

/**
* Controller to retrieve an AssessmentAssignmentSet by assessment ID.
*/
export const getAssignmentSetController = async (
req: Request,
res: Response
) => {
const { assessmentId } = req.params;

try {
const assignmentSet = await getAssignmentSetByAssessmentId(assessmentId);
res
.status(200)
.json(
assignmentSet.assignedTeams === null
? assignmentSet.assignedUsers
: assignmentSet.assignedTeams
);
} catch (error) {
if (error instanceof NotFoundError) {
res.status(404).json({ error: error.message });
} else {
console.error('Error fetching AssessmentAssignmentSet:', error);
res
.status(500)
.json({ error: 'Failed to fetch AssessmentAssignmentSet' });
}
}
};

/**
* Controller to update the assignedTeams within an AssessmentAssignmentSet.
*/
export const updateAssignmentSetController = async (
req: Request,
res: Response
) => {
const { assessmentId } = req.params;
const { assignedTeams, assignedUsers } = req.body;

try {
const updatedSet = await updateAssignmentSet(
assessmentId,
assignedTeams,
assignedUsers
);
res.status(200).json(updatedSet);
} catch (error) {
if (error instanceof NotFoundError || error instanceof BadRequestError) {
res.status(400).json({ error: error.message });
} else {
console.error('Error updating AssessmentAssignmentSet:', error);
res
.status(500)
.json({ error: 'Failed to update AssessmentAssignmentSet' });
}
}
};

/**
* Controller to retrieve all teams assigned to a specific TA within an assessment.
*/
export const getAssignmentsByTAIdController = async (
req: Request,
res: Response
) => {
const { assessmentId } = req.params;
const accountId = await getAccountId(req);
const userId = await getUserIdByAccountId(accountId);

try {
const assignments = await getAssignmentsByTAId(userId, assessmentId);
res.status(200).json(assignments);
} catch (error) {
if (error instanceof NotFoundError) {
res.status(404).json({ error: error.message });
} else {
console.error('Error fetching assignments by TA:', error);
res.status(500).json({ error: 'Failed to fetch assignments by TA' });
}
}
};

/**
* Controller to retrieve all unmarked teams assigned to a specific TA within an assessment.
*/
export const getUnmarkedAssignmentsByTAIdController = async (
req: Request,
res: Response
) => {
const { assessmentId } = req.params;
const accountId = await getAccountId(req);
const userId = await getUserIdByAccountId(accountId);

try {
const assignments = await getUnmarkedAssignmentsByTAId(
userId,
assessmentId
);
res.status(200).json(assignments);
} catch (error) {
if (error instanceof NotFoundError) {
res.status(404).json({ error: error.message });
} else {
console.error('Error fetching assignments by TA:', error);
res.status(500).json({ error: 'Failed to fetch assignments by TA' });
}
}
};
164 changes: 164 additions & 0 deletions backend/controllers/assessmentResultController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response } from 'express';
import {
getOrCreateAssessmentResults,
recalculateResult,
} from '../services/assessmentResultService';
import { BadRequestError, NotFoundError } from '../services/errors';
import { getAssignmentSetByAssessmentId } from '../services/assessmentAssignmentSetService';
import { getAccountId } from '../utils/auth';
import { getInternalAssessmentById } from '../services/internalAssessmentService';
import TeamModel from '@models/Team';

/**
* Controller to retrieve or create AssessmentResults for an assessment.
* Route: GET /api/assessment-results/:assessmentId
*/
export const getOrCreateAssessmentResultsController = async (
req: Request,
res: Response
) => {
const { assessmentId } = req.params;
const accountId = await getAccountId(req);

try {
const assessment = await getInternalAssessmentById(assessmentId, accountId);
if (!assessment) {
throw new NotFoundError('Assessment not found');
}

const assignmentSet = await getAssignmentSetByAssessmentId(assessmentId);
if (!assignmentSet) {
throw new NotFoundError('Assignment Set not found for this assessment');
}

const assessmentResults = await getOrCreateAssessmentResults(assessmentId);
if (!assessmentResults) {
throw new NotFoundError(
'Assessment Results not found for this assessment'
);
}

const assessmentResultMap = new Map<string, any>();

assessmentResults.forEach(result => {
assessmentResultMap.set(result.student._id.toString(), result);
});

if (assessment.granularity === 'individual') {
if (!assignmentSet.assignedUsers) {
throw new BadRequestError(
'Assessment is individual granularity, but assignments are for teams'
);
}
for (const assignedUser of assignmentSet.assignedUsers) {
const studentId = assignedUser.user._id.toString();
let assessmentResult = assessmentResultMap.get(studentId);
if (!assessmentResult) {
assessmentResult = {
_id: 'temp-' + studentId,
assessment: assessmentId,
student: assignedUser.user,
marks: [],
};
}
for (const marker of assignedUser.tas) {
const existingMarkEntry = assessmentResult.marks.find(
(markEntry: any) =>
markEntry.marker._id.toString() === marker._id.toString()
);
if (!existingMarkEntry) {
const tempMarkEntry = {
marker: marker,
submission: null,
score: null,
};
assessmentResult.marks.push(tempMarkEntry);
}
}
assessmentResultMap.set(studentId, assessmentResult);
}
} else if (assessment.granularity === 'team') {
if (!assignmentSet.assignedTeams) {
throw new BadRequestError(
'Assessment is team granularity, but assignments are for users'
);
}
for (const assignedTeam of assignmentSet.assignedTeams) {
const teamId = assignedTeam.team._id.toString();
const team = await TeamModel.findById(teamId).populate('members');
if (!team || !team.members || team.members.length === 0) {
continue;
}
team.members.forEach(member => {
const studentId = member._id.toString();
let assessmentResult = assessmentResultMap.get(studentId);
if (!assessmentResult) {
assessmentResult = {
_id: 'temp-team-' + studentId,
assessment: assessmentId,
team: assignedTeam.team,
marks: [],
};
}
for (const marker of assignedTeam.tas) {
const existingMarkEntry = assessmentResult.marks.find(
(markEntry: any) =>
markEntry.marker._id.toString() === marker._id.toString()
);
if (!existingMarkEntry) {
const tempMarkEntry = {
marker: marker,
submission: null,
score: null,
};
assessmentResult.marks.push(tempMarkEntry);
}
}
assessmentResultMap.set(studentId, assessmentResult);
});
}
}

const updatedAssessmentResults = Array.from(assessmentResultMap.values());

res.json({ data: updatedAssessmentResults });
} catch (error) {
if (error instanceof BadRequestError) {
res.status(400).json({ error: error.message });
} else if (error instanceof NotFoundError) {
res.status(404).json({ error: error.message });
} else {
console.error('Error retrieving or creating AssessmentResults:', error);
res
.status(500)
.json({ error: 'Failed to retrieve or create AssessmentResults' });
}
}
};

/**
* Controller to recalculate the average score of an AssessmentResult.
* Route: POST /api/assessment-results/:resultId/recalculate
*/
export const recalculateResultController = async (
req: Request,
res: Response
) => {
const { resultId } = req.params;

try {
await recalculateResult(resultId);
res.status(200).json({
success: true,
message: 'Average score recalculated successfully.',
});
} catch (error) {
if (error instanceof NotFoundError) {
res.status(404).json({ error: error.message });
} else {
console.error('Error recalculating AssessmentResult:', error);
res.status(500).json({ error: 'Failed to recalculate AssessmentResult' });
}
}
};
Loading
Loading