diff --git a/api/.nvmrc b/api/.nvmrc
index ea438c37..d3e33db8 100644
--- a/api/.nvmrc
+++ b/api/.nvmrc
@@ -1 +1 @@
-v18.14
+v18.16
diff --git a/api/.yarnrc b/api/.yarnrc
new file mode 100644
index 00000000..3b7b0fb6
--- /dev/null
+++ b/api/.yarnrc
@@ -0,0 +1 @@
+workspaces-experimental false
diff --git a/api/Dockerfile b/api/Dockerfile
index 7350cc54..caa11372 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -1,11 +1,11 @@
-FROM node:18.14-alpine AS build
+FROM node:18.16-alpine AS build
WORKDIR /app/
COPY package.json yarn.lock ./
RUN yarn install --ignore-platform --frozen-lockfile --network-timeout 600000
COPY ./ ./
RUN yarn build
-FROM node:18.14-alpine
+FROM node:18.16-alpine
WORKDIR /app/
COPY --from=build /app/dist/ ./dist/
COPY --from=build /app/package.json /app/yarn.lock ./
diff --git a/api/package.json b/api/package.json
index 25bf673b..8a427344 100644
--- a/api/package.json
+++ b/api/package.json
@@ -1,9 +1,9 @@
{
"name": "@rhizone-lms/api",
- "version": "0.0.0",
+ "version": "0.5.0",
"private": true,
"engines": {
- "node": "^18.14.0",
+ "node": "^18.16.0",
"yarn": "^1.22.19"
},
"scripts": {
@@ -19,44 +19,43 @@
"test:watch": "jest --watchAll"
},
"dependencies": {
- "connect-redis": "^6.0.0",
+ "connect-redis": "^7.1.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-session": "^1.17.2",
- "helmet": "^6.0.1",
+ "helmet": "^7.0.0",
"knex": "^2.4.2",
- "luxon": "^3.2.1",
- "mysql": "^2.18.1",
- "redis": "^3.1.2",
+ "luxon": "^3.3.0",
+ "mysql2": "^3.3.1",
+ "redis": "^4.6.6",
"rollbar": "^2.26.1",
- "socket.io": "^4.6.0",
+ "socket.io": "^4.6.1",
"superagent": "^8.0.9"
},
"devDependencies": {
- "@types/connect-redis": "^0.0.19",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
- "@types/express-session": "^1.17.4",
- "@types/jest": "^29.4.0",
- "@types/luxon": "^3.2.0",
- "@types/mock-knex": "^0.4.3",
+ "@types/express-session": "^1.17.7",
+ "@types/jest": "^29.5.1",
+ "@types/luxon": "^3.3.0",
+ "@types/mock-knex": "^0.4.5",
"@types/mysql": "^2.15.19",
- "@types/node": "^18.13.0",
- "@types/superagent": "^4.1.13",
+ "@types/node": "^20.2.3",
+ "@types/superagent": "^4.1.17",
"@types/supertest": "^2.0.11",
- "@typescript-eslint/eslint-plugin": "^5.51.0",
- "@typescript-eslint/parser": "^5.51.0",
+ "@typescript-eslint/eslint-plugin": "^5.59.7",
+ "@typescript-eslint/parser": "^5.59.7",
"dotenv": "^16.0.3",
- "eslint": "^8.33.0",
- "eslint-config-prettier": "^8.6.0",
- "jest": "^29.4.2",
+ "eslint": "^8.41.0",
+ "eslint-config-prettier": "^8.8.0",
+ "jest": "^29.5.0",
"mock-knex": "^0.4.12",
- "nodemon": "^2.0.15",
- "prettier": "^2.8.4",
+ "nodemon": "^2.0.22",
+ "prettier": "^2.8.8",
"supertest": "^6.3.3",
- "ts-jest": "^29.0.5",
+ "ts-jest": "^29.1.0",
"ts-node": "^10.4.0",
- "typescript": "^4.9.5"
+ "typescript": "^5.0.4"
},
"eslintConfig": {
"env": {
diff --git a/api/src/middleware/__tests__/assessmentsRouter.ts b/api/src/middleware/__tests__/assessmentsRouter.ts
new file mode 100644
index 00000000..15face38
--- /dev/null
+++ b/api/src/middleware/__tests__/assessmentsRouter.ts
@@ -0,0 +1,3345 @@
+import {
+ collectionEnvelope,
+ errorEnvelope,
+ itemEnvelope,
+} from '../responseEnvelope';
+import { createAppAgentForRouter, mockPrincipalId } from '../routerTestUtils';
+
+import {
+ AssessmentDetails,
+ AssessmentSubmission,
+ AssessmentWithSubmissions,
+ AssessmentWithSummary,
+ CurriculumAssessment,
+ FacilitatorAssessmentSubmissionsSummary,
+ ParticipantAssessmentSubmissionsSummary,
+ ProgramAssessment,
+ SavedAssessment,
+} from '../../models';
+import {
+ constructFacilitatorAssessmentSummary,
+ constructParticipantAssessmentSummary,
+ createAssessmentSubmission,
+ createCurriculumAssessment,
+ createProgramAssessment,
+ deleteCurriculumAssessment,
+ deleteProgramAssessment,
+ enrollFacilitator,
+ enrollParticipant,
+ facilitatorProgramIdsMatchingCurriculum,
+ findProgramAssessment,
+ getAssessmentSubmission,
+ getCurriculumAssessment,
+ getPrincipalProgramRole,
+ listAllProgramAssessmentSubmissions,
+ listParticipantProgramAssessmentSubmissions,
+ listPrincipalEnrolledProgramIds,
+ listProgramAssessments,
+ updateAssessmentSubmission,
+ updateCurriculumAssessment,
+ updateProgramAssessment,
+} from '../../services/assessmentsService';
+
+import assessmentsRouter from '../assessmentsRouter';
+
+jest.mock('../../services/assessmentsService');
+
+const mockConstructFacilitatorAssessmentSummary = jest.mocked(
+ constructFacilitatorAssessmentSummary
+);
+const mockConstructParticipantAssessmentSummary = jest.mocked(
+ constructParticipantAssessmentSummary
+);
+const mockCreateAssessmentSubmission = jest.mocked(createAssessmentSubmission);
+const mockCreateCurriculumAssessment = jest.mocked(createCurriculumAssessment);
+const mockCreateProgramAssessment = jest.mocked(createProgramAssessment);
+const mockDeleteCurriculumAssessment = jest.mocked(deleteCurriculumAssessment);
+const mockDeleteProgramAssessment = jest.mocked(deleteProgramAssessment);
+const mockFindProgramAssessment = jest.mocked(findProgramAssessment);
+const mockEnrollFacilitator = jest.mocked(enrollFacilitator);
+const mockEnrollParticipant = jest.mocked(enrollParticipant);
+const mockGetAssessmentSubmission = jest.mocked(getAssessmentSubmission);
+const mockGetCurriculumAssessment = jest.mocked(getCurriculumAssessment);
+const mockFacilitatorProgramIdsMatchingCurriculum = jest.mocked(
+ facilitatorProgramIdsMatchingCurriculum
+);
+const mockGetPrincipalProgramRole = jest.mocked(getPrincipalProgramRole);
+const mockListAllProgramAssessmentSubmissions = jest.mocked(
+ listAllProgramAssessmentSubmissions
+);
+const mockListParticipantProgramAssessmentSubmissions = jest.mocked(
+ listParticipantProgramAssessmentSubmissions
+);
+const mockListPrincipalEnrolledProgramIds = jest.mocked(
+ listPrincipalEnrolledProgramIds
+);
+const mockListProgramAssessments = jest.mocked(listProgramAssessments);
+const mockUpdateAssessmentSubmission = jest.mocked(updateAssessmentSubmission);
+const mockUpdateCurriculumAssessment = jest.mocked(updateCurriculumAssessment);
+const mockUpdateProgramAssessment = jest.mocked(updateProgramAssessment);
+
+/* EXAMPLE DATA: Variables */
+
+const participantPrincipalId = 30;
+const unenrolledPrincipalId = 31;
+const otherParticipantPrincipalId = 32;
+const facilitatorPrincipalId = 300;
+const curriculumId = 4;
+const curriculumAssessmentId = 8;
+const assessmentSubmissionId = 32;
+const facilitatorProgramIdsThatMatchCurriculum = [12, 20, 30];
+
+/* EXAMPLE DATA: Database Rows */
+
+const programAssessmentsRows = [
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2050-06-24 00:00:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2023-02-10 00:00:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2050-06-24 00:00:00',
+ due_date: '2050-06-23 00:00:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2050-06-26 00:00:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2050-06-24 00:00:00',
+ },
+];
+
+const assessmentResponsesRows = [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: null as number,
+ response: null as string,
+ score: null as number,
+ grader_response: null as string,
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ response: null as string,
+ score: null as number,
+ grader_response: null as string,
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ response: null as string,
+ score: null as number,
+ grader_response: null as string,
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ response: null as string,
+ score: 1,
+ grader_response: 'Well done!',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: null as number,
+ response: null as string,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: null as number,
+ response: '
Hello world!
',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: null as number,
+ response: 'Hello world!
',
+ score: 0,
+ grader_response: 'Very close!',
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ score: null as number,
+ grader_response: 'Well done!',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response: 'Hello world!
',
+ score: 0,
+ grader_response: 'Very close!',
+ },
+];
+
+/* EXAMPLE DATA: Structured Data */
+
+const curriculumAssessments: CurriculumAssessment[] = [
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 3,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null,
+ question_type: 'free response',
+ answers: [
+ {
+ id: 29,
+ question_id: 24,
+ description: null,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 29,
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null as string,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ questions: [
+ {
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ answers: [
+ {
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ questions: [
+ {
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null,
+ question_type: 'free response',
+ answers: [
+ {
+ description: null,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 121,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ answers: [
+ {
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ answers: [
+ {
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: 'Also known as a DBMS.',
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null as string,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ id: 9,
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ id: 9,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ id: 9,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null,
+ question_type: 'free response',
+ answers: [
+ {
+ id: 29,
+ question_id: 24,
+ description: null,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 29,
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+];
+
+const programAssessments: ProgramAssessment[] = [
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2050-06-24T00:00:00.000-07:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2023-02-10T00:00:00.000-08:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2050-06-24T00:00:00.000-07:00',
+ due_date: '2050-06-23T00:00:00.000-07:00',
+ },
+ {
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2050-06-24 00:00:00',
+ },
+];
+
+const assessmentDetails: AssessmentDetails[] = [
+ {
+ curriculum_assessment: {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ program_assessment: {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2050-06-24T00:00:00.000-07:00',
+ },
+ },
+];
+
+const participantSummaries: ParticipantAssessmentSubmissionsSummary[] = [
+ {
+ principal_id: 30,
+ highest_state: 'Inactive',
+ total_num_submissions: 0,
+ },
+ {
+ principal_id: 30,
+ highest_state: 'Expired',
+ total_num_submissions: 1,
+ },
+ {
+ principal_id: 30,
+ highest_state: 'Active',
+ total_num_submissions: 0,
+ },
+ {
+ principal_id: 30,
+ highest_state: 'Graded',
+ most_recent_submitted_date: '2023-02-09T13:23:45.000Z',
+ total_num_submissions: 1,
+ highest_score: 4,
+ },
+];
+
+const facilitatorSummaries: FacilitatorAssessmentSubmissionsSummary[] = [
+ {
+ num_participants_with_submissions: 8,
+ num_program_participants: 12,
+ num_ungraded_submissions: 6,
+ },
+];
+
+const assessmentSubmissions: AssessmentSubmission[] = [
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:00:00.000Z',
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:00:00.000Z',
+ responses: [
+ { id: 320, assessment_id: 16, submission_id: 32, question_id: 24 },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-10T07:00:00.000Z',
+ last_modified: '2023-02-10T07:05:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Expired',
+ opened_at: '2023-02-10T07:00:00.000Z',
+ last_modified: '2023-02-17T08:00:10.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Expired',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T14:00:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Expired',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-16T14:00:10.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 36,
+ assessment_id: 16,
+ principal_id: 32,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:01:00.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Graded',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ score: 4,
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Graded',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ score: 4,
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ score: 1,
+ grader_response: 'Well done!',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Graded',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-10T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ id: 320,
+ score: null as number,
+ grader_response: null as string,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-10T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ id: 321,
+ score: null as number,
+ grader_response: null as string,
+ },
+ ],
+ },
+ {
+ id: 33,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-10T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ id: 320,
+ score: null as number,
+ grader_response: null as string,
+ },
+ ],
+ },
+];
+
+const assessmentsWithSubmissions: AssessmentWithSubmissions[] = [
+ {
+ curriculum_assessment: {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ },
+ program_assessment: {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2050-06-24T00:00:00.000-07:00',
+ },
+ principal_program_role: 'Participant',
+ submissions: [
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ curriculum_assessment: {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ },
+ program_assessment: {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2050-06-24T00:00:00.000-07:00',
+ },
+ principal_program_role: 'Facilitator',
+ submissions: [
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 36,
+ assessment_id: 16,
+ principal_id: 32,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:01:00.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ },
+ ],
+ },
+];
+
+const savedAssessments: SavedAssessment[] = [
+ {
+ curriculum_assessment: {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ },
+ ],
+ },
+ ],
+ },
+ program_assessment: {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2050-06-24T00:00:00.000-07:00',
+ },
+ principal_program_role: 'Participant',
+ submission: {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:00:00.000Z',
+ },
+ },
+ {
+ curriculum_assessment: {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 3,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ program_assessment: {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2050-06-24T00:00:00.000-07:00',
+ },
+ principal_program_role: 'Participant',
+ submission: {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:00:00.000Z',
+ },
+ },
+];
+
+describe('assessmentsRouter', () => {
+ const appAgent = createAppAgentForRouter(assessmentsRouter);
+
+ describe('GET /', () => {
+ it('should respond with an empty list for a user not enrolled in any programs', done => {
+ mockListPrincipalEnrolledProgramIds.mockResolvedValue([]);
+
+ mockPrincipalId(unenrolledPrincipalId);
+
+ appAgent.get('/').expect(200, collectionEnvelope([], 0), err => {
+ expect(mockListPrincipalEnrolledProgramIds).toHaveBeenCalledWith(
+ unenrolledPrincipalId
+ );
+ done(err);
+ });
+ });
+
+ it('should respond with a list of all assessments (without questions) for participant enrolled in one program', done => {
+ const ParticipantAssessmentSubmissionsSummary: AssessmentWithSummary[] = [
+ {
+ curriculum_assessment: curriculumAssessments[0],
+ program_assessment: programAssessments[0],
+ participant_submissions_summary: participantSummaries[3],
+ principal_program_role: 'Participant',
+ },
+ ];
+ mockListPrincipalEnrolledProgramIds.mockResolvedValue([
+ programAssessments[0].program_id,
+ ]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockListProgramAssessments.mockResolvedValue([programAssessments[0]]);
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]);
+ mockConstructParticipantAssessmentSummary.mockResolvedValue(
+ participantSummaries[3]
+ );
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get('/')
+ .expect(
+ 200,
+ collectionEnvelope(
+ ParticipantAssessmentSubmissionsSummary,
+ ParticipantAssessmentSubmissionsSummary.length
+ ),
+ err => {
+ expect(mockListPrincipalEnrolledProgramIds).toHaveBeenCalledWith(
+ participantPrincipalId
+ );
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+ expect(mockListProgramAssessments).toHaveBeenCalledWith(
+ programAssessments[0].program_id
+ );
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ false,
+ false
+ );
+ expect(
+ mockConstructParticipantAssessmentSummary
+ ).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0]
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a list of all assessments (without questions) for facilitator of one program', done => {
+ const facilitatorAssessmentListResponse: AssessmentWithSummary[] = [
+ {
+ curriculum_assessment: curriculumAssessments[0],
+ program_assessment: programAssessments[0],
+ facilitator_submissions_summary: facilitatorSummaries[0],
+ principal_program_role: 'Facilitator',
+ },
+ ];
+ mockListPrincipalEnrolledProgramIds.mockResolvedValue([
+ programAssessments[0].program_id,
+ ]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockListProgramAssessments.mockResolvedValue([programAssessments[0]]);
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]);
+ mockConstructFacilitatorAssessmentSummary.mockResolvedValue(
+ facilitatorSummaries[0]
+ );
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .get('/')
+ .expect(
+ 200,
+ collectionEnvelope(
+ facilitatorAssessmentListResponse,
+ facilitatorAssessmentListResponse.length
+ ),
+ err => {
+ expect(mockListPrincipalEnrolledProgramIds).toHaveBeenCalledWith(
+ facilitatorPrincipalId
+ );
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+ expect(mockListProgramAssessments).toBeCalledWith(
+ programAssessments[0].program_id
+ );
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ false,
+ false
+ );
+ expect(
+ mockConstructFacilitatorAssessmentSummary
+ ).toHaveBeenCalledWith(programAssessments[0]);
+ done(err);
+ }
+ );
+ });
+
+ it('should throw an error if a database error was encountered', done => {
+ mockListPrincipalEnrolledProgramIds.mockRejectedValue(new Error());
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent.get('/').expect(500, done);
+ });
+ });
+
+ describe('GET /demo/facilitator', () => {
+ it('should enroll a user as a facilitator in a program, then redirect to assessments list page', done => {
+ mockEnrollFacilitator.mockResolvedValue(true);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent.get(`/demo/facilitator`).expect(302, done);
+ });
+
+ it('should do nothing if the user is already a facilitator', done => {
+ mockEnrollFacilitator.mockResolvedValue(false);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent.get(`/demo/facilitator`).expect(204, done);
+ });
+
+ it('should throw an error if a database error was encountered', done => {
+ mockEnrollFacilitator.mockRejectedValue(new Error());
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent.get(`/demo/facilitator`).expect(500, done);
+ });
+ });
+
+ describe('GET /demo/participant', () => {
+ it('should enroll a user as a participant in a program, then redirect to assessments list page', done => {
+ mockEnrollParticipant.mockResolvedValue(true);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent.get(`/demo/participant`).expect(302, done);
+ });
+
+ it('should do nothing if the user is already a participant', done => {
+ mockEnrollParticipant.mockResolvedValue(false);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent.get(`/demo/participant`).expect(204, done);
+ });
+
+ it('should throw an error if a database error was encountered', done => {
+ mockEnrollParticipant.mockRejectedValue(new Error());
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent.get(`/demo/participant`).expect(500, done);
+ });
+ });
+
+ describe('GET /curriculum/:curriculumAssessmentId', () => {
+ it('should retrieve a curriculum assessment if the logged-in principal ID is the program facilitator', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]);
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([
+ programAssessments[0].program_id,
+ ]);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .get(`/curriculum/${curriculumAssessments[3].id}`)
+ .expect(200, itemEnvelope(curriculumAssessments[3]), err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[3].id,
+ true,
+ true
+ );
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ curriculumAssessments[3].curriculum_id
+ );
+
+ done(err);
+ });
+ });
+
+ it('should respond with an UnauthorizedError if the logged-in principal ID is not the program facilitator', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]);
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/curriculum/${curriculumAssessments[3].id}`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Not allowed to access curriculum assessment with ID ${curriculumAssessments[3].id}.`
+ ),
+ err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[3].id,
+ true,
+ true
+ );
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(
+ participantPrincipalId,
+ curriculumAssessments[3].curriculum_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an BadRequestError if the curriculum assessment ID is not a number', done => {
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .get(`/curriculum/test`)
+ .expect(
+ 400,
+ errorEnvelope(`"${Number(test)}" is not a valid submission ID.`),
+ err => {
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a NotFoundError if the curriculum assessment ID was not found in the database', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .get(`/curriculum/${curriculumAssessments[3].id}`)
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find curriculum assessment with ID ${curriculumAssessments[3].id}.`
+ ),
+ err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[3].id,
+ true,
+ true
+ );
+
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('POST /curriculum', () => {
+ it('should create a curriculum assessment if the logged-in principal ID is the program facilitator', done => {
+ const matchingFacilitatorPrograms = [3, 4, 6];
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue(
+ matchingFacilitatorPrograms
+ );
+ mockCreateCurriculumAssessment.mockResolvedValue(
+ curriculumAssessments[13]
+ );
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .post(`/curriculum`)
+ .send(curriculumAssessments[5])
+ .expect(201, err => {
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ curriculumAssessments[5].curriculum_id
+ );
+ expect(mockCreateCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[5]
+ );
+
+ done(err);
+ });
+ });
+
+ it('should respond with an Unauthorized Error if the logged-in principal ID is not the facilitator', done => {
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([]);
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .post(`/curriculum`)
+ .send(curriculumAssessments[5])
+ .expect(
+ 401,
+ errorEnvelope(
+ `Not allowed to add a new assessment for this curriculum.`
+ ),
+ err => {
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(
+ participantPrincipalId,
+ curriculumAssessments[5].curriculum_id
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should reponse with BadRequestError if the information missing', done => {
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .post(`/curriculum`)
+ .send({ description: 'test' })
+ .expect(
+ 422,
+ errorEnvelope(`Was not given a valid curriculum assessment.`),
+ err => {
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('PUT /curriculum/:curriculumAssessmentId', () => {
+ it('should update a curriculum assessment if the logged-in principal ID is the program facilitator', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]);
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue(
+ facilitatorProgramIdsThatMatchCurriculum
+ );
+ mockUpdateCurriculumAssessment.mockResolvedValue(
+ curriculumAssessments[8]
+ );
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .put(`/curriculum/${curriculumAssessments[0].id}`)
+ .send(curriculumAssessments[8])
+ .expect(200, itemEnvelope(curriculumAssessments[8]), err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[0].id
+ );
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(facilitatorPrincipalId, curriculumId);
+ expect(mockUpdateCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[8]
+ );
+
+ done(err);
+ });
+ });
+
+ it('should respond with a BadRequestError if the curriculumAssessment ID is not a valid number', done => {
+ const curriculumAssessmentIdInvalid = 0;
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .put(`/curriculum/${curriculumAssessmentIdInvalid}`)
+ .expect(
+ 400,
+ errorEnvelope(
+ `"${Number(
+ curriculumAssessmentIdInvalid
+ )}" is not a valid curriculum assessment ID.`
+ ),
+ done
+ );
+ });
+
+ it('should respond with a ValidationError if given an curriculum assessment is not valid', done => {
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .put(`/curriculum/${curriculumAssessments[0].id}`)
+ .send({ assessment_type: 'test' })
+ .expect(
+ 422,
+ errorEnvelope(`Was not given a valid curriculum assessment.`),
+ err => {
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a NotFoundError if the curriculum assessment ID was not found in the database', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(null);
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .put(`/curriculum/${curriculumAssessments[0].id}`)
+ .send(curriculumAssessments[8])
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find curriculum assessment with ID ${curriculumAssessments[0].id}.`
+ ),
+ err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[0].id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an Unauthorized Error if the the facilitator is not taking the program with the curriculum', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]);
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .put(`/curriculum/${curriculumAssessments[0].id}`)
+ .send(curriculumAssessments[8])
+ .expect(
+ 401,
+ errorEnvelope(
+ `Not allowed to make modifications to curriculum assessment with ID ${curriculumAssessments[0].id}.`
+ ),
+ err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[0].id
+ );
+
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(participantPrincipalId, curriculumId);
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with InternalServerError if curriculum assessment with given ID could not be updated', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]);
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue(
+ facilitatorProgramIdsThatMatchCurriculum
+ );
+ mockUpdateCurriculumAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .put(`/curriculum/${curriculumAssessments[0].id}`)
+ .send(curriculumAssessments[8])
+
+ .expect(
+ 500,
+ errorEnvelope(
+ `Could not update curriculum assessment with ID ${curriculumAssessments[0].id}.`
+ ),
+ err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[0].id
+ );
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(facilitatorPrincipalId, curriculumId);
+ expect(mockUpdateCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[8]
+ );
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('DELETE /curriculum/:curriculumAssessmentId', () => {
+ it('should delete a curriculumAssessment if principal ID is a program facilitator of that curriculum', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]);
+
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue(
+ facilitatorProgramIdsThatMatchCurriculum
+ );
+ mockDeleteCurriculumAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .delete(`/curriculum/${curriculumAssessmentId}`)
+ .expect(204, {}, err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessmentId
+ );
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(facilitatorPrincipalId, curriculumId);
+
+ expect(mockDeleteCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[0].id
+ );
+
+ done(err);
+ });
+ });
+
+ it('should respond with an Unauthorized Error if the the facilitator is not taking the program with the curriculum', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]);
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue([]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .delete(`/curriculum/${curriculumAssessmentId}`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Not allowed to delete curriculum assessment with ID ${curriculumAssessmentId}.`
+ ),
+ err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessmentId
+ );
+
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(participantPrincipalId, curriculumId);
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a BadRequestError if the curriculumAssessment ID is not a valid number', done => {
+ const curriculumId = 'test';
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .delete(`/curriculum/${curriculumId}`)
+ .expect(
+ 400,
+ errorEnvelope(
+ `"${Number(curriculumId)}" is not a valid curriculum assessment ID.`
+ ),
+ done
+ );
+ });
+
+ it('should respond with a NotFoundError if the curriculum assessment ID was not found in the database', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .delete(`/curriculum/${curriculumAssessmentId}`)
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find curriculum assessment with ID ${curriculumAssessmentId}.`
+ ),
+ err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessmentId
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a ConflictError if trying to delete curriculum Assessment ID that has participant submissions', done => {
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]);
+ mockFacilitatorProgramIdsMatchingCurriculum.mockResolvedValue(
+ facilitatorProgramIdsThatMatchCurriculum
+ );
+ mockDeleteCurriculumAssessment.mockRejectedValue(new Error());
+
+ mockPrincipalId(facilitatorPrincipalId);
+ appAgent
+ .delete(`/curriculum/${curriculumAssessmentId}`)
+ .expect(
+ 409,
+ errorEnvelope(`Cannot delete a curriculum assessment.`),
+ err => {
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessmentId
+ );
+
+ expect(
+ mockFacilitatorProgramIdsMatchingCurriculum
+ ).toHaveBeenCalledWith(facilitatorPrincipalId, curriculumId);
+
+ expect(mockDeleteCurriculumAssessment).toHaveBeenCalledWith(
+ curriculumAssessments[0].id
+ );
+
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('GET /program/:programAssessmentId', () => {
+ it('should get a program assessment if the logged-in principal ID is the program facilitator', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}`)
+ .expect(200, itemEnvelope(assessmentDetails[0]), err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ true
+ );
+
+ done(err);
+ });
+ });
+
+ it('should respond with an Unauthorized Error if the logged-in principal ID is not the facilitator', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue(null);
+
+ mockPrincipalId(otherParticipantPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Could not access assessment with Program Assessment ID ${programAssessments[0].id}.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ otherParticipantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an BadRequestError if the program assessment ID is not a number', done => {
+ const exampleAssessmentFromUser = 'test';
+
+ mockPrincipalId(otherParticipantPrincipalId);
+
+ appAgent
+ .get(`/program/${exampleAssessmentFromUser}`)
+ .expect(
+ 400,
+ errorEnvelope(
+ `"${Number(
+ exampleAssessmentFromUser
+ )}" is not a valid submission ID.`
+ ),
+ done
+ );
+ });
+
+ it('should respond with a NotFoundError if the program assessment ID was not found in the database', done => {
+ const programAssessmentId = 20;
+
+ mockFindProgramAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(facilitatorPrincipalId);
+ appAgent
+ .get(`/program/${programAssessmentId}`)
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find program assessment with ID ${programAssessmentId}.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessmentId
+ );
+
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('POST /program', () => {
+ it('should create a new program assessment if the logged-in principal ID is the program facilitator', done => {
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockCreateProgramAssessment.mockResolvedValue(programAssessmentsRows[3]);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .post(`/program`)
+ .send(programAssessmentsRows[0])
+ .expect(201, err => {
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessmentsRows[0].program_id
+ );
+
+ expect(mockCreateProgramAssessment).toHaveBeenCalledWith(
+ programAssessmentsRows[0]
+ );
+
+ done(err);
+ });
+ });
+
+ it('should respond with an Unauthorized Error if the logged-in principal ID is not the facilitator', done => {
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .post(`/program`)
+ .send(programAssessmentsRows[0])
+ .expect(
+ 401,
+ errorEnvelope(
+ `User is not allowed to create new program assessments for this program.`
+ ),
+ err => {
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessmentsRows[0].program_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a BadRequestError if given an invalid program assessment', done => {
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .post(`/program`)
+ .send({ available_after: '2023-08-10' })
+ .expect(
+ 400,
+ errorEnvelope(`Was not given a valid program assessment.`),
+ err => {
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('PUT /program/:programAssessmentId', () => {
+ it('should update a program assessment if the logged-in principal ID is the program facilitator', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockUpdateProgramAssessment.mockResolvedValue(programAssessmentsRows[3]);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .put(`/program/${programAssessments[0].id}`)
+ .send(programAssessmentsRows[3])
+ .expect(200, itemEnvelope(programAssessmentsRows[3]), err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockUpdateProgramAssessment).toHaveBeenCalledWith(
+ programAssessmentsRows[3]
+ );
+
+ done(err);
+ });
+ });
+
+ it('should respond with an Unauthorized Error if the logged-in principal ID is not the facilitator', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue(null);
+
+ mockPrincipalId(otherParticipantPrincipalId);
+
+ appAgent
+ .put(`/program/${programAssessments[0].id}`)
+ .send(programAssessmentsRows[3])
+ .expect(
+ 401,
+ errorEnvelope(
+ `Could not access program Assessment with ID ${programAssessments[0].id}.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ otherParticipantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an BadRequestError if the program assessment ID is not a number', done => {
+ const exampleAssessmentFromUser = 'test';
+
+ mockPrincipalId(otherParticipantPrincipalId);
+
+ appAgent
+ .put(`/program/${exampleAssessmentFromUser}`)
+ .send(programAssessments[0])
+ .expect(
+ 400,
+ errorEnvelope(
+ `"${Number(
+ exampleAssessmentFromUser
+ )}" is not a valid program assessment ID.`
+ ),
+ done
+ );
+ });
+
+ it('should respond with an BadRequestError if not given a valid program assessment', done => {
+ const exampleAssessmentFormUser = 'test';
+
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .put(`/program/${programAssessments[0].id}`)
+ .send(exampleAssessmentFormUser)
+ .expect(
+ 400,
+ errorEnvelope(`Was not given a valid program assessment.`),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a NotFoundError if the program assessment ID was not found in the database', done => {
+ const programAssessmentId = 20;
+
+ mockFindProgramAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(facilitatorPrincipalId);
+ appAgent
+ .put(`/program/${programAssessmentId}`)
+ .send(programAssessmentsRows[3])
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find program assessment with ID ${programAssessmentId}.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessmentId
+ );
+
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('DELETE /program/:programAssessmentId', () => {
+ it('should delete a program assessment in the system if logged-in user is facilitator of that program', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockDeleteProgramAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(facilitatorPrincipalId);
+ appAgent
+ .delete(`/program/${programAssessments[0].id}`)
+ .expect(204, {}, err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+ expect(mockDeleteProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+ done(err);
+ });
+ });
+
+ it('should return an error if logged-in user is not a facilitator of that program', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+
+ mockPrincipalId(participantPrincipalId);
+ appAgent
+ .delete(`/program/${programAssessments[0].id}`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Not allowed to access program assessment with ID ${programAssessments[0].id}.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a BadRequestError if given an invalid program assessment ID', done => {
+ const programAssessmentId = 'test';
+
+ appAgent
+ .delete(`/program/${programAssessmentId}`)
+ .expect(
+ 400,
+ errorEnvelope(
+ `"${Number(
+ programAssessmentId
+ )}" is not a valid program assessment ID.`
+ ),
+ done
+ );
+ });
+ it('should respond with a NotFoundError if assessment ID not exist', done => {
+ mockFindProgramAssessment.mockResolvedValue(null);
+ appAgent
+ .delete(`/program/${programAssessments[0].id}`)
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find program assessment with ID ${programAssessments[0].id}.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+ done(err);
+ }
+ );
+ });
+ it('should respond with a ConflictError if trying to delete program assessment that has participant submissions', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockDeleteProgramAssessment.mockRejectedValue(new Error());
+ mockPrincipalId(facilitatorPrincipalId);
+ appAgent
+ .delete(`/program/${programAssessments[0].id}`)
+ .expect(
+ 409,
+ errorEnvelope(
+ `Cannot delete a program assessment that has participant submissions.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+ expect(mockDeleteProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('GET /program/:programAssessmentId/submissions', () => {
+ it('should show a facilitator an AssessmentWithSubmissions with all participant submissions', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockListAllProgramAssessmentSubmissions.mockResolvedValue([
+ assessmentSubmissions[2],
+ assessmentSubmissions[11],
+ ]);
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions`)
+ .expect(200, itemEnvelope(assessmentsWithSubmissions[1]), err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockListAllProgramAssessmentSubmissions).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ false,
+ false
+ );
+
+ done(err);
+ });
+ });
+
+ it('should show a participant an AssessmentWithSubmissions with their submissions', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockListParticipantProgramAssessmentSubmissions.mockResolvedValue([
+ assessmentSubmissions[2],
+ ]);
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[0]);
+
+ mockPrincipalId(participantPrincipalId);
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions`)
+ .expect(200, itemEnvelope(assessmentsWithSubmissions[0]), err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(
+ mockListParticipantProgramAssessmentSubmissions
+ ).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ false,
+ false
+ );
+
+ done(err);
+ });
+ });
+
+ it('should give an UnauthorizedError to anyone not enrolled in the course', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue(null);
+
+ mockPrincipalId(unenrolledPrincipalId);
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Could not access program assessment with ID ${programAssessments[0].id} without enrollment.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ unenrolledPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should give a NotFoundError for a programAssessmentId not found in the database', done => {
+ mockFindProgramAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(participantPrincipalId);
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions`)
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find program assessment with ID ${programAssessments[0].id}`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should give a BadRequestError for an invalid programAssessmentId', done => {
+ mockPrincipalId(participantPrincipalId);
+ appAgent
+ .get(`/program/test/submissions`)
+ .expect(
+ 400,
+ errorEnvelope(`"test" is not a valid program assessment ID.`),
+ err => {
+ done(err);
+ }
+ );
+ });
+ });
+
+ describe('GET /program/:programAssessmentId/submissions/new', () => {
+ it('should respond with a bad request error if given an invalid assessment id', done => {
+ const programAssessmentInvalidId = 'test';
+ appAgent
+ .get(`/program/${programAssessmentInvalidId}/submissions/new`)
+ .expect(
+ 400,
+ errorEnvelope(
+ `"${programAssessmentInvalidId}" is not a valid program assessment ID.`
+ ),
+ done
+ );
+ });
+
+ it('should respond with a NotFoundError if the assessment submission ID was not found in the database', done => {
+ mockFindProgramAssessment.mockResolvedValue(null);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions/new`)
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find program assessment with ID ${programAssessments[0].id}.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should return an error when attempting to create a submission for a program assessment that is not yet available', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[2]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[2].id}/submissions/new`)
+ .expect(
+ 403,
+ errorEnvelope(
+ `Could not create a new submission of an assessment that's not yet available.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[2].id
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should return an error when attempting to create a submission when the program assessment due date has passed', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[1]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[1].id}/submissions/new`)
+ .expect(
+ 403,
+ errorEnvelope(
+ `Could not create a new submission of an assessment after its due date.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[1].id
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should return an error if logged-in user is not enrolled in the program', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue(null);
+
+ mockPrincipalId(unenrolledPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions/new`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Could not access program assessment with ID ${programAssessments[0].id}) without enrollment.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ unenrolledPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should return an error if logged-in user is a facilitator', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions/new`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Facilitators are not allowed to create program assessment submissions.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should return an error if no possible submissions remain for this participant and this assessment', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]);
+ mockListParticipantProgramAssessmentSubmissions.mockResolvedValue([
+ assessmentSubmissions[11],
+ ]);
+
+ mockPrincipalId(otherParticipantPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions/new`)
+ .expect(
+ 403,
+ errorEnvelope(
+ `Could not create a new submission as you have reached the maximum number of submissions for this assessment.`
+ ),
+ err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ otherParticipantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ false
+ );
+
+ expect(
+ mockListParticipantProgramAssessmentSubmissions
+ ).toHaveBeenCalledWith(
+ otherParticipantPrincipalId,
+ programAssessments[0].id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should return the existing submission if one is currently opened or in progress', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]);
+ mockListParticipantProgramAssessmentSubmissions.mockResolvedValue([
+ assessmentSubmissions[0],
+ ]);
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[0]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions/new`)
+ .expect(200, itemEnvelope(savedAssessments[0]), err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ false
+ );
+
+ expect(
+ mockListParticipantProgramAssessmentSubmissions
+ ).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].id
+ );
+
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[2].id,
+ true,
+ false
+ );
+
+ done(err);
+ });
+ });
+
+ it('should return a participant a new submission without including the correct answers', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]);
+ mockListParticipantProgramAssessmentSubmissions.mockResolvedValue(null);
+ mockCreateAssessmentSubmission.mockResolvedValue(
+ assessmentSubmissions[0]
+ );
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions/new`)
+ .expect(201, itemEnvelope(savedAssessments[0]), err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ false
+ );
+
+ expect(
+ mockListParticipantProgramAssessmentSubmissions
+ ).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].id
+ );
+
+ expect(mockCreateAssessmentSubmission).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].id,
+ programAssessments[0].assessment_id
+ );
+
+ done(err);
+ });
+ });
+
+ it('should return a participant a new submission without including the correct answers, even if a prior submitted submission exists', done => {
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[2]);
+ mockListParticipantProgramAssessmentSubmissions.mockResolvedValue([
+ assessmentSubmissions[9],
+ ]);
+ mockCreateAssessmentSubmission.mockResolvedValue(
+ assessmentSubmissions[0]
+ );
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/program/${programAssessments[0].id}/submissions/new`)
+ .expect(201, itemEnvelope(savedAssessments[1]), err => {
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ false
+ );
+
+ expect(
+ mockListParticipantProgramAssessmentSubmissions
+ ).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].id
+ );
+
+ expect(mockCreateAssessmentSubmission).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].id,
+ programAssessments[0].assessment_id
+ );
+
+ done(err);
+ });
+ });
+ });
+
+ describe('GET /submissions/:submissionId', () => {
+ it("should show a facilitator the full submission information for a participant's ungraded submitted assessment, including the correct answers", done => {
+ const facilitatorFullResponse: SavedAssessment = {
+ curriculum_assessment: curriculumAssessments[3],
+ program_assessment: programAssessments[0],
+ principal_program_role: 'Facilitator',
+ submission: assessmentSubmissions[9],
+ };
+
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]);
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .get(`/submissions/${assessmentSubmissions[9].id}`)
+ .expect(200, itemEnvelope(facilitatorFullResponse), err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true
+ );
+
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ true
+ );
+
+ done(err);
+ });
+ });
+
+ it('should show a participant their submission information for an in-progress assessment without including the correct answers', done => {
+ const participantInProgressAssessmentSubmission: SavedAssessment = {
+ curriculum_assessment: curriculumAssessments[1],
+ program_assessment: programAssessments[0],
+ principal_program_role: 'Participant',
+ submission: assessmentSubmissions[2],
+ };
+
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[2]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/submissions/${assessmentSubmissions[2].id}`)
+ .expect(
+ 200,
+ itemEnvelope(participantInProgressAssessmentSubmission),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[2].id,
+ true
+ );
+
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ false
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should show a participant their submission information for an ungraded submitted assessment without including the correct answers', done => {
+ const participantSubmittedAssessmentSubmission: SavedAssessment = {
+ curriculum_assessment: curriculumAssessments[1],
+ program_assessment: programAssessments[0],
+ principal_program_role: 'Participant',
+ submission: assessmentSubmissions[9],
+ };
+
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[1]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/submissions/${assessmentSubmissions[9].id}`)
+ .expect(
+ 200,
+ itemEnvelope(participantSubmittedAssessmentSubmission),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true
+ );
+
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ false
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should show a participant their submission information for a graded submitted assessment including the correct answers', done => {
+ const participantGradedAssessmentSubmission: SavedAssessment = {
+ curriculum_assessment: curriculumAssessments[3],
+ program_assessment: programAssessments[0],
+ principal_program_role: 'Participant',
+ submission: assessmentSubmissions[13],
+ };
+
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[13]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockGetCurriculumAssessment.mockResolvedValue(curriculumAssessments[3]);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/submissions/${assessmentSubmissions[13].id}`)
+ .expect(
+ 200,
+ itemEnvelope(participantGradedAssessmentSubmission),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[13].id,
+ true
+ );
+
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetCurriculumAssessment).toHaveBeenCalledWith(
+ programAssessments[0].assessment_id,
+ true,
+ true
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a BadRequestError if given an invalid submission ID', done => {
+ const submissionId = 'test';
+
+ appAgent
+ .get(`/submissions/${submissionId}`)
+ .expect(
+ 400,
+ errorEnvelope(
+ `"${Number(submissionId)}" is not a valid submission ID.`
+ ),
+ done
+ );
+ });
+
+ it('should respond with a NotFoundError if the submission ID was not found in the database', done => {
+ const submissionId = 8;
+ mockGetAssessmentSubmission.mockResolvedValue(null);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/submissions/${submissionId}`)
+ .expect(
+ 404,
+ errorEnvelope(`Could not find submission with ID ${submissionId}.`),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ submissionId,
+ true
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an Unauthorized Error if the logged-in principal ID is not the same as the principal ID of the submission ID and is not the principal ID of the program facilitator', done => {
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+
+ mockPrincipalId(otherParticipantPrincipalId);
+
+ appAgent
+ .get(`/submissions/${assessmentSubmissions[9].id}`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Could not access submission with ID ${assessmentSubmissions[9].id}.`
+ ),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true
+ );
+
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ otherParticipantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an Unauthorized Error if logged-in principal ID is not enrolled in the program', done => {
+ const programId = 12;
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue(null);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .get(`/submissions/${assessmentSubmissions[9].id}`)
+ .expect(
+ 401,
+ errorEnvelope(
+ `Could not access submission with ID ${assessmentSubmissions[9].id}.`
+ ),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true
+ );
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programId
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an internal server error if a database error occurs', done => {
+ const submissionId = 10;
+ mockGetAssessmentSubmission.mockRejectedValue(new Error());
+ appAgent.get(`/submissions/${submissionId}`).expect(500, done);
+ });
+ });
+
+ describe('PUT /submissions/:submissionId', () => {
+ it('should update submission if the logged-in principal ID is the program facilitator', done => {
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Facilitator');
+ mockUpdateAssessmentSubmission.mockResolvedValue(
+ assessmentSubmissions[17]
+ );
+
+ mockPrincipalId(facilitatorPrincipalId);
+
+ appAgent
+ .put(`/submissions/${assessmentSubmissions[9].id}`)
+ .send(assessmentSubmissions[17])
+ .expect(200, itemEnvelope(assessmentSubmissions[17]), err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true
+ );
+
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ assessmentSubmissions[17].assessment_id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockUpdateAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[17],
+ true
+ );
+
+ done(err);
+ });
+ });
+
+ it('should update submission if the logged-in principal ID is the program participant', done => {
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[2]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+ mockUpdateAssessmentSubmission.mockResolvedValue(
+ assessmentSubmissions[2]
+ );
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .put(`/submissions/${assessmentSubmissions[2].id}`)
+ .send(assessmentSubmissions[2])
+ .expect(200, itemEnvelope(assessmentSubmissions[2]), err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[2].id,
+ true
+ );
+
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ assessmentSubmissions[2].assessment_id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockUpdateAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[2],
+ false
+ );
+
+ done(err);
+ });
+ });
+
+ it('should do nothing if the logged-in principal ID is the program participant and the assessment was already submitted', done => {
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .put(`/submissions/${assessmentSubmissions[9].id}`)
+ .send(assessmentSubmissions[9])
+ .expect(200, itemEnvelope(assessmentSubmissions[9]), err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true
+ );
+
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ assessmentSubmissions[9].assessment_id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true,
+ false
+ );
+
+ done(err);
+ });
+ });
+
+ it('should respond with a BadRequestError if given an invalid submission ID', done => {
+ const submissionId = 'test';
+
+ appAgent
+ .put(`/submissions/${submissionId}`)
+ .send(assessmentResponsesRows[7])
+ .expect(
+ 400,
+ errorEnvelope(
+ `"${Number(submissionId)}" is not a valid submission ID.`
+ ),
+ done
+ );
+ });
+
+ it('should respond with a ValidationError if given an invalid AssessmentSubmission', done => {
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .put(`/submissions/${assessmentSubmissions[9].id}`)
+ .send({ test: 'Hello' })
+ .expect(
+ 422,
+ errorEnvelope(`Was not given a valid assessment submission.`),
+ done
+ );
+ });
+
+ it('should respond with a NotFoundError if the submission ID was not found in the database', done => {
+ mockGetAssessmentSubmission.mockResolvedValue(null);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .put(`/submissions/${assessmentSubmissionId}`)
+ .send(assessmentSubmissions[13])
+ .expect(
+ 404,
+ errorEnvelope(
+ `Could not find submission with ID ${assessmentSubmissionId}.`
+ ),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissionId,
+ true
+ );
+
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an Unauthorized Error if user is not a member of the program of the submission', done => {
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue(null);
+
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .put(`/submissions/${assessmentSubmissions[9].id}`)
+ .send(assessmentSubmissions[13])
+ .expect(
+ 401,
+ errorEnvelope(
+ `Could not access the assessment and submssion without enrollment in the program or being a facilitator.`
+ ),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true
+ );
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an Unauthorized Error if user is participant but not the owner of the submission', done => {
+ mockGetAssessmentSubmission.mockResolvedValue(assessmentSubmissions[9]);
+ mockFindProgramAssessment.mockResolvedValue(programAssessments[0]);
+ mockGetPrincipalProgramRole.mockResolvedValue('Participant');
+
+ mockPrincipalId(otherParticipantPrincipalId);
+
+ appAgent
+ .put(`/submissions/${assessmentSubmissions[9].id}`)
+ .send(assessmentSubmissions[13])
+ .expect(
+ 401,
+ errorEnvelope(
+ `You may not update an assessment that is not your own.`
+ ),
+ err => {
+ expect(mockGetAssessmentSubmission).toHaveBeenCalledWith(
+ assessmentSubmissions[9].id,
+ true
+ );
+ expect(mockFindProgramAssessment).toHaveBeenCalledWith(
+ programAssessments[0].id
+ );
+
+ expect(mockGetPrincipalProgramRole).toHaveBeenCalledWith(
+ otherParticipantPrincipalId,
+ programAssessments[0].program_id
+ );
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with a BadRequestError if submssion ID from param is not the same from request body', done => {
+ mockPrincipalId(participantPrincipalId);
+
+ appAgent
+ .put(`/submissions/${assessmentSubmissionId}`)
+ .send(assessmentSubmissions[19])
+ .expect(
+ 400,
+ errorEnvelope(
+ `The submission ID in the parameter (${assessmentSubmissionId}) is not the same ID as in the request body (${assessmentSubmissions[19].id}).`
+ ),
+ err => {
+ done(err);
+ }
+ );
+ });
+
+ it('should respond with an internal server error if a database error occurs', done => {
+ mockGetAssessmentSubmission.mockRejectedValue(new Error());
+ appAgent
+ .put(`/submissions/${assessmentSubmissionId}`)
+ .send(assessmentSubmissions[13])
+ .expect(500, done);
+ });
+ });
+});
diff --git a/api/src/middleware/assessmentsRouter.ts b/api/src/middleware/assessmentsRouter.ts
new file mode 100644
index 00000000..7c955d11
--- /dev/null
+++ b/api/src/middleware/assessmentsRouter.ts
@@ -0,0 +1,979 @@
+import { Router } from 'express';
+import { DateTime } from 'luxon';
+
+import {
+ BadRequestError,
+ ConflictError,
+ ForbiddenError,
+ InternalServerError,
+ NotFoundError,
+ UnauthorizedError,
+ ValidationError,
+} from './httpErrors';
+import { collectionEnvelope, itemEnvelope } from './responseEnvelope';
+
+import {
+ AssessmentDetails,
+ AssessmentSubmission,
+ AssessmentWithSubmissions,
+ AssessmentWithSummary,
+ CurriculumAssessment,
+ ProgramAssessment,
+ SavedAssessment,
+} from '../models';
+import {
+ constructFacilitatorAssessmentSummary,
+ constructParticipantAssessmentSummary,
+ createAssessmentSubmission,
+ createCurriculumAssessment,
+ createProgramAssessment,
+ deleteCurriculumAssessment,
+ deleteProgramAssessment,
+ enrollFacilitator,
+ enrollParticipant,
+ facilitatorProgramIdsMatchingCurriculum,
+ findProgramAssessment,
+ getAssessmentSubmission,
+ getCurriculumAssessment,
+ getPrincipalProgramRole,
+ listAllProgramAssessmentSubmissions,
+ listParticipantProgramAssessmentSubmissions,
+ listPrincipalEnrolledProgramIds,
+ listProgramAssessments,
+ updateAssessmentSubmission,
+ updateCurriculumAssessment,
+ updateProgramAssessment,
+} from '../services/assessmentsService';
+import { findConfig } from '../services/configService';
+
+const assessmentsRouter = Router();
+
+assessmentsRouter.get('/', async (req, res, next) => {
+ const { principalId } = req.session;
+
+ try {
+ const programIds = await listPrincipalEnrolledProgramIds(principalId);
+
+ const assessmentsSummaryList: AssessmentWithSummary[] = [];
+
+ if (!programIds || programIds.length === 0) {
+ res.json(collectionEnvelope(assessmentsSummaryList, 0));
+ return;
+ }
+
+ for (const programId of programIds) {
+ const roleInProgram = await getPrincipalProgramRole(
+ principalId,
+ programId
+ );
+ const programAssessments = await listProgramAssessments(programId);
+
+ for (const programAssessment of programAssessments) {
+ if (roleInProgram === 'Participant') {
+ assessmentsSummaryList.push({
+ curriculum_assessment: await getCurriculumAssessment(
+ programAssessment.assessment_id,
+ false,
+ false
+ ),
+ program_assessment: programAssessment,
+ participant_submissions_summary:
+ await constructParticipantAssessmentSummary(
+ principalId,
+ programAssessment
+ ),
+ principal_program_role: roleInProgram,
+ });
+ }
+
+ if (roleInProgram === 'Facilitator') {
+ assessmentsSummaryList.push({
+ curriculum_assessment: await getCurriculumAssessment(
+ programAssessment.assessment_id,
+ false,
+ false
+ ),
+ program_assessment: programAssessment,
+ facilitator_submissions_summary:
+ await constructFacilitatorAssessmentSummary(programAssessment),
+ principal_program_role: roleInProgram,
+ });
+ }
+ }
+ }
+ res.json(
+ collectionEnvelope(assessmentsSummaryList, assessmentsSummaryList.length)
+ );
+ } catch (error) {
+ next(error);
+ return;
+ }
+});
+
+assessmentsRouter.get('/demo/facilitator', async (req, res, next) => {
+ const { principalId } = req.session;
+
+ try {
+ if (await enrollFacilitator(principalId, 2)) {
+ res.redirect(findConfig('WEBAPP_ORIGIN', '') + '/assessments');
+ } else {
+ res.status(204).send();
+ }
+ } catch (error) {
+ next(error);
+ return;
+ }
+});
+
+assessmentsRouter.get('/demo/participant', async (req, res, next) => {
+ const { principalId } = req.session;
+
+ try {
+ if (await enrollParticipant(principalId, 2)) {
+ res.redirect(findConfig('WEBAPP_ORIGIN', '') + '/assessments');
+ } else {
+ res.status(204).send();
+ }
+ } catch (error) {
+ next(error);
+ return;
+ }
+});
+
+assessmentsRouter.get(
+ '/curriculum/:curriculumAssessmentId',
+ async (req, res, next) => {
+ const { principalId } = req.session;
+ const { curriculumAssessmentId } = req.params;
+ const curriculumAssessmentIdParsed = Number(curriculumAssessmentId);
+
+ if (
+ !Number.isInteger(curriculumAssessmentIdParsed) ||
+ curriculumAssessmentIdParsed < 1
+ ) {
+ next(
+ new BadRequestError(
+ `"${curriculumAssessmentIdParsed}" is not a valid submission ID.`
+ )
+ );
+ return;
+ }
+
+ try {
+ const includeQuestionsAndAllAnswers = true;
+ const includeQuestionsAndCorrectAnswers = true;
+
+ const curriculumAssessment = await getCurriculumAssessment(
+ curriculumAssessmentIdParsed,
+ includeQuestionsAndAllAnswers,
+ includeQuestionsAndCorrectAnswers
+ );
+
+ if (!curriculumAssessment) {
+ throw new NotFoundError(
+ `Could not find curriculum assessment with ID ${curriculumAssessmentIdParsed}.`
+ );
+ }
+
+ const matchingProgramIds = await facilitatorProgramIdsMatchingCurriculum(
+ principalId,
+ curriculumAssessment.curriculum_id
+ );
+
+ if (matchingProgramIds.length === 0) {
+ throw new UnauthorizedError(
+ `Not allowed to access curriculum assessment with ID ${curriculumAssessmentIdParsed}.`
+ );
+ }
+
+ res.json(itemEnvelope(curriculumAssessment));
+ } catch (err) {
+ next(err);
+ return;
+ }
+ }
+);
+
+assessmentsRouter.post('/curriculum', async (req, res, next) => {
+ const { principalId } = req.session;
+ const curriculumAssessmentFromUser = req.body;
+
+ const isACurriculumAssessment = (
+ possibleAssessment: unknown
+ ): possibleAssessment is CurriculumAssessment => {
+ return (possibleAssessment as CurriculumAssessment).title !== undefined;
+ };
+ if (!isACurriculumAssessment(curriculumAssessmentFromUser)) {
+ next(new ValidationError(`Was not given a valid curriculum assessment.`));
+ return;
+ }
+
+ try {
+ const facilitatorProgramIds = await facilitatorProgramIdsMatchingCurriculum(
+ principalId,
+ curriculumAssessmentFromUser.curriculum_id
+ );
+
+ if (facilitatorProgramIds.length === 0) {
+ throw new UnauthorizedError(
+ `Not allowed to add a new assessment for this curriculum.`
+ );
+ }
+
+ const curriculumAssessment = await createCurriculumAssessment(
+ curriculumAssessmentFromUser
+ );
+
+ res.status(201).json(itemEnvelope(curriculumAssessment));
+ } catch (error) {
+ next(error);
+ return;
+ }
+});
+
+assessmentsRouter.put(
+ '/curriculum/:curriculumAssessmentId',
+ async (req, res, next) => {
+ const { principalId } = req.session;
+ const { curriculumAssessmentId } = req.params;
+ const curriculumAssessmentIdParsed = Number(curriculumAssessmentId);
+
+ if (
+ !Number.isInteger(curriculumAssessmentIdParsed) ||
+ curriculumAssessmentIdParsed < 1
+ ) {
+ next(
+ new BadRequestError(
+ `"${curriculumAssessmentIdParsed}" is not a valid curriculum assessment ID.`
+ )
+ );
+ return;
+ }
+
+ const curriculumAssessmentFromUser = req.body;
+
+ const isACurriculumAssessment = (
+ possibleAssessment: unknown
+ ): possibleAssessment is CurriculumAssessment => {
+ return (possibleAssessment as CurriculumAssessment).id !== undefined;
+ };
+
+ if (!isACurriculumAssessment(curriculumAssessmentFromUser)) {
+ next(new ValidationError(`Was not given a valid curriculum assessment.`));
+ return;
+ }
+
+ try {
+ const curriculumAssessmentExisting = await getCurriculumAssessment(
+ curriculumAssessmentIdParsed
+ );
+
+ if (!curriculumAssessmentExisting) {
+ throw new NotFoundError(
+ `Could not find curriculum assessment with ID ${curriculumAssessmentIdParsed}.`
+ );
+ }
+
+ const matchingProgramAssessments =
+ await facilitatorProgramIdsMatchingCurriculum(
+ principalId,
+ curriculumAssessmentExisting.curriculum_id
+ );
+
+ if (matchingProgramAssessments.length === 0) {
+ throw new UnauthorizedError(
+ `Not allowed to make modifications to curriculum assessment with ID ${curriculumAssessmentIdParsed}.`
+ );
+ }
+
+ const updatedCurriculumAssessment: CurriculumAssessment =
+ await updateCurriculumAssessment(curriculumAssessmentFromUser);
+
+ if (!updatedCurriculumAssessment) {
+ throw new InternalServerError(
+ `Could not update curriculum assessment with ID ${curriculumAssessmentIdParsed}.`
+ );
+ }
+
+ res.json(itemEnvelope(updatedCurriculumAssessment));
+ } catch (err) {
+ next(err);
+ return;
+ }
+ }
+);
+
+assessmentsRouter.delete(
+ '/curriculum/:curriculumAssessmentId',
+ async (req, res, next) => {
+ const { principalId } = req.session;
+ const { curriculumAssessmentId } = req.params;
+ const curriculumAssessmentIdParsed = Number(curriculumAssessmentId);
+
+ if (
+ !Number.isInteger(curriculumAssessmentIdParsed) ||
+ curriculumAssessmentIdParsed < 1
+ ) {
+ next(
+ new BadRequestError(
+ `"${curriculumAssessmentIdParsed}" is not a valid curriculum assessment ID.`
+ )
+ );
+ return;
+ }
+
+ try {
+ const curriculumAssessmentExisting = await getCurriculumAssessment(
+ curriculumAssessmentIdParsed
+ );
+
+ if (!curriculumAssessmentExisting) {
+ throw new NotFoundError(
+ `Could not find curriculum assessment with ID ${curriculumAssessmentIdParsed}.`
+ );
+ }
+
+ const matchingPrograms = await facilitatorProgramIdsMatchingCurriculum(
+ principalId,
+ curriculumAssessmentExisting.curriculum_id
+ );
+
+ if (matchingPrograms.length === 0) {
+ throw new UnauthorizedError(
+ `Not allowed to delete curriculum assessment with ID ${curriculumAssessmentIdParsed}.`
+ );
+ }
+
+ await deleteCurriculumAssessment(curriculumAssessmentIdParsed).catch(
+ () => {
+ throw new ConflictError(`Cannot delete a curriculum assessment.`);
+ }
+ );
+
+ res.status(204).send();
+ } catch (err) {
+ next(err);
+ return;
+ }
+ }
+);
+
+assessmentsRouter.get(
+ '/program/:programAssessmentId',
+ async (req, res, next) => {
+ const { principalId } = req.session;
+ const { programAssessmentId } = req.params;
+ const programAssessmentIdParsed = Number(programAssessmentId);
+
+ if (
+ !Number.isInteger(programAssessmentIdParsed) ||
+ programAssessmentIdParsed < 1
+ ) {
+ next(
+ new BadRequestError(
+ `"${programAssessmentIdParsed}" is not a valid submission ID.`
+ )
+ );
+ return;
+ }
+
+ try {
+ const programAssessment = await findProgramAssessment(
+ programAssessmentIdParsed
+ );
+
+ if (!programAssessment) {
+ throw new NotFoundError(
+ `Could not find program assessment with ID ${programAssessmentIdParsed}.`
+ );
+ }
+
+ const programRole = await getPrincipalProgramRole(
+ principalId,
+ programAssessment.program_id
+ );
+
+ if (programRole !== 'Facilitator') {
+ throw new UnauthorizedError(
+ `Could not access assessment with Program Assessment ID ${programAssessmentIdParsed}.`
+ );
+ }
+
+ const includeQuestionsAndAllAnswers = true;
+ const includeQuestionsAndCorrectAnswers = true;
+
+ const curriculumAssessment = await getCurriculumAssessment(
+ programAssessment.assessment_id,
+ includeQuestionsAndAllAnswers,
+ includeQuestionsAndCorrectAnswers
+ );
+
+ const assessmentDetails: AssessmentDetails = {
+ curriculum_assessment: curriculumAssessment,
+ program_assessment: programAssessment,
+ };
+
+ res.json(itemEnvelope(assessmentDetails));
+ } catch (err) {
+ next(err);
+ return;
+ }
+ }
+);
+
+assessmentsRouter.post('/program', async (req, res, next) => {
+ const { principalId } = req.session;
+ const programAssessmentFromUser = req.body;
+
+ let programAssessment;
+ try {
+ const isProgramAssessment = (
+ possibleAssessment: unknown
+ ): possibleAssessment is ProgramAssessment => {
+ return (possibleAssessment as ProgramAssessment).program_id !== undefined;
+ };
+
+ if (!isProgramAssessment(programAssessmentFromUser)) {
+ throw new BadRequestError(`Was not given a valid program assessment.`);
+ }
+
+ const programRole = await getPrincipalProgramRole(
+ principalId,
+ programAssessmentFromUser.program_id
+ );
+
+ if (programRole !== 'Facilitator') {
+ throw new UnauthorizedError(
+ `User is not allowed to create new program assessments for this program.`
+ );
+ }
+
+ programAssessment = await createProgramAssessment(
+ programAssessmentFromUser
+ );
+ res.status(201).json(itemEnvelope(programAssessment));
+ } catch (error) {
+ next(error);
+ return;
+ }
+});
+
+assessmentsRouter.put(
+ '/program/:programAssessmentId',
+ async (req, res, next) => {
+ const { programAssessmentId } = req.params;
+ const { principalId } = req.session;
+ const programAssessmentFromUser = req.body;
+ const programAssessmentIdParsed = Number(programAssessmentId);
+
+ if (
+ !Number.isInteger(programAssessmentIdParsed) ||
+ programAssessmentIdParsed < 1
+ ) {
+ next(
+ new BadRequestError(
+ `"${programAssessmentIdParsed}" is not a valid program assessment ID.`
+ )
+ );
+ return;
+ }
+
+ let updatedPrgramAssessment;
+ try {
+ const programAssessment = await findProgramAssessment(
+ programAssessmentIdParsed
+ );
+
+ if (programAssessment === null) {
+ throw new NotFoundError(
+ `Could not find program assessment with ID ${programAssessmentIdParsed}.`
+ );
+ }
+
+ const programRole = await getPrincipalProgramRole(
+ principalId,
+ programAssessment.program_id
+ );
+
+ if (programRole !== 'Facilitator') {
+ next(
+ new UnauthorizedError(
+ `Could not access program Assessment with ID ${programAssessmentIdParsed}.`
+ )
+ );
+ return;
+ }
+
+ const isprogramAssessment = (
+ possibleAssessment: unknown
+ ): possibleAssessment is ProgramAssessment => {
+ return (possibleAssessment as ProgramAssessment).id !== undefined;
+ };
+
+ if (!isprogramAssessment(programAssessmentFromUser)) {
+ next(new BadRequestError(`Was not given a valid program assessment.`));
+ return;
+ }
+
+ updatedPrgramAssessment = await updateProgramAssessment(
+ programAssessmentFromUser
+ );
+ res.json(itemEnvelope(updatedPrgramAssessment));
+ } catch (error) {
+ next(error);
+ return;
+ }
+ }
+);
+
+assessmentsRouter.delete(
+ '/program/:programAssessmentId',
+ async (req, res, next) => {
+ const { principalId } = req.session;
+ const { programAssessmentId } = req.params;
+ const programAssessmentIdParsed = Number(programAssessmentId);
+
+ if (
+ !Number.isInteger(programAssessmentIdParsed) ||
+ programAssessmentIdParsed < 1
+ ) {
+ next(
+ new BadRequestError(
+ `"${programAssessmentIdParsed}" is not a valid program assessment ID.`
+ )
+ );
+ return;
+ }
+
+ try {
+ const matchingProgramAssessment = await findProgramAssessment(
+ programAssessmentIdParsed
+ );
+
+ if (matchingProgramAssessment === null) {
+ throw new NotFoundError(
+ `Could not find program assessment with ID ${programAssessmentIdParsed}.`
+ );
+ }
+
+ const programRole = await getPrincipalProgramRole(
+ principalId,
+ matchingProgramAssessment.program_id
+ );
+
+ if (programRole !== 'Facilitator') {
+ throw new UnauthorizedError(
+ `Not allowed to access program assessment with ID ${programAssessmentIdParsed}.`
+ );
+ }
+
+ await deleteProgramAssessment(programAssessmentIdParsed).catch(() => {
+ throw new ConflictError(
+ `Cannot delete a program assessment that has participant submissions.`
+ );
+ });
+ } catch (err) {
+ next(err);
+ return;
+ }
+
+ res.status(204).send();
+ }
+);
+
+assessmentsRouter.get(
+ '/program/:programAssessmentId/submissions',
+ async (req, res, next) => {
+ const { principalId } = req.session;
+ const { programAssessmentId } = req.params;
+ const programAssessmentIdParsed = Number(programAssessmentId);
+ if (
+ !Number.isInteger(programAssessmentIdParsed) ||
+ programAssessmentIdParsed < 1
+ ) {
+ next(
+ new BadRequestError(
+ `"${programAssessmentId}" is not a valid program assessment ID.`
+ )
+ );
+ return;
+ }
+
+ let curriculumAssessment: CurriculumAssessment;
+ let programAssessment: ProgramAssessment;
+ let principalProgramRole: string;
+ let submissions: AssessmentSubmission[];
+
+ try {
+ programAssessment = await findProgramAssessment(
+ programAssessmentIdParsed
+ );
+
+ if (!programAssessment) {
+ throw new NotFoundError(
+ `Could not find program assessment with ID ${programAssessmentIdParsed}`
+ );
+ }
+
+ const programRole = await getPrincipalProgramRole(
+ principalId,
+ programAssessment.program_id
+ );
+
+ switch (programRole) {
+ case 'Participant':
+ submissions = await listParticipantProgramAssessmentSubmissions(
+ principalId,
+ programAssessment.id
+ );
+ principalProgramRole = 'Participant';
+ break;
+ case 'Facilitator':
+ submissions = await listAllProgramAssessmentSubmissions(
+ programAssessment.id
+ );
+ principalProgramRole = 'Facilitator';
+ break;
+ default:
+ throw new UnauthorizedError(
+ `Could not access program assessment with ID ${programAssessmentIdParsed} without enrollment.`
+ );
+ }
+
+ const includeQuestionsAndAllAnswers = false;
+ const includeQuestionsAndCorrectAnswers = false;
+
+ curriculumAssessment = await getCurriculumAssessment(
+ programAssessment.assessment_id,
+ includeQuestionsAndAllAnswers,
+ includeQuestionsAndCorrectAnswers
+ );
+
+ const assessmentWithSubmissions: AssessmentWithSubmissions = {
+ curriculum_assessment: curriculumAssessment,
+ program_assessment: programAssessment,
+ principal_program_role: principalProgramRole,
+ submissions,
+ };
+
+ res.json(itemEnvelope(assessmentWithSubmissions));
+ } catch (error) {
+ next(error);
+ return;
+ }
+ }
+);
+
+assessmentsRouter.get(
+ '/program/:programAssessmentId/submissions/new',
+ async (req, res, next) => {
+ const { principalId } = req.session;
+ const { programAssessmentId } = req.params;
+ const programAssessmentIdParsed = Number(programAssessmentId);
+
+ if (
+ !Number.isInteger(programAssessmentIdParsed) ||
+ programAssessmentIdParsed < 1
+ ) {
+ next(
+ new BadRequestError(
+ `"${programAssessmentId}" is not a valid program assessment ID.`
+ )
+ );
+ return;
+ }
+
+ try {
+ const programAssessment = await findProgramAssessment(
+ programAssessmentIdParsed
+ );
+
+ if (!programAssessment) {
+ throw new NotFoundError(
+ `Could not find program assessment with ID ${programAssessmentIdParsed}.`
+ );
+ }
+
+ if (
+ DateTime.fromISO(programAssessment.available_after) > DateTime.now()
+ ) {
+ throw new ForbiddenError(
+ `Could not create a new submission of an assessment that's not yet available.`
+ );
+ }
+
+ if (DateTime.fromISO(programAssessment.due_date) < DateTime.now()) {
+ throw new ForbiddenError(
+ `Could not create a new submission of an assessment after its due date.`
+ );
+ }
+
+ const programRole = await getPrincipalProgramRole(
+ principalId,
+ programAssessment.program_id
+ );
+
+ if (!programRole) {
+ throw new UnauthorizedError(
+ `Could not access program assessment with ID ${programAssessmentIdParsed}) without enrollment.`
+ );
+ }
+
+ if (programRole === 'Facilitator') {
+ throw new UnauthorizedError(
+ `Facilitators are not allowed to create program assessment submissions.`
+ );
+ }
+
+ const includeQuestionsAndAllAnswers = true;
+ const includeQuestionsAndCorrectAnswers = false;
+ const curriculumAssessment = await getCurriculumAssessment(
+ programAssessment.assessment_id,
+ includeQuestionsAndAllAnswers,
+ includeQuestionsAndCorrectAnswers
+ );
+
+ const existingAssessmentSubmissions =
+ await listParticipantProgramAssessmentSubmissions(
+ principalId,
+ programAssessment.id
+ );
+
+ let assessmentSubmission: AssessmentSubmission;
+ let submissionCreated = false;
+
+ if (!existingAssessmentSubmissions) {
+ assessmentSubmission = await createAssessmentSubmission(
+ principalId,
+ programAssessmentIdParsed,
+ programAssessment.assessment_id
+ );
+ submissionCreated = true;
+ } else {
+ const inProgressSubmissions: AssessmentSubmission[] =
+ existingAssessmentSubmissions.filter(assessmentSubmission =>
+ ['Opened', 'In Progress'].includes(
+ assessmentSubmission.assessment_submission_state
+ )
+ );
+ if (inProgressSubmissions.length !== 0) {
+ assessmentSubmission = await getAssessmentSubmission(
+ inProgressSubmissions[0].id,
+ true,
+ false
+ );
+ } else if (
+ existingAssessmentSubmissions.length >=
+ curriculumAssessment.max_num_submissions &&
+ inProgressSubmissions.length === 0
+ ) {
+ throw new ForbiddenError(
+ `Could not create a new submission as you have reached the maximum number of submissions for this assessment.`
+ );
+ } else {
+ assessmentSubmission = await createAssessmentSubmission(
+ principalId,
+ programAssessmentIdParsed,
+ programAssessment.assessment_id
+ );
+ submissionCreated = true;
+ }
+ }
+
+ const assessmentWithSubmission: SavedAssessment = {
+ curriculum_assessment: curriculumAssessment,
+ program_assessment: programAssessment,
+ principal_program_role: programRole,
+ submission: assessmentSubmission,
+ };
+
+ if (submissionCreated === true) {
+ res.status(201).json(itemEnvelope(assessmentWithSubmission));
+ } else {
+ res.json(itemEnvelope(assessmentWithSubmission));
+ }
+ } catch (err) {
+ next(err);
+ return;
+ }
+ }
+);
+
+assessmentsRouter.get('/submissions/:submissionId', async (req, res, next) => {
+ const { principalId } = req.session;
+ const { submissionId } = req.params;
+ const submissionIdParsed = Number(submissionId);
+
+ if (!Number.isInteger(submissionIdParsed) || submissionIdParsed < 1) {
+ next(
+ new BadRequestError(
+ `"${submissionIdParsed}" is not a valid submission ID.`
+ )
+ );
+ return;
+ }
+
+ try {
+ const assessmentSubmission = await getAssessmentSubmission(
+ submissionIdParsed,
+ true
+ );
+
+ if (!assessmentSubmission) {
+ next(
+ new NotFoundError(
+ `Could not find submission with ID ${submissionIdParsed}.`
+ )
+ );
+ return;
+ }
+
+ const programAssessmentId = assessmentSubmission.assessment_id;
+ const programAssessment = await findProgramAssessment(programAssessmentId);
+
+ const programRole = await getPrincipalProgramRole(
+ principalId,
+ programAssessment.program_id
+ );
+
+ if (!programRole) {
+ next(
+ new UnauthorizedError(
+ `Could not access submission with ID ${submissionIdParsed}.`
+ )
+ );
+ return;
+ }
+
+ if (programRole === 'Participant') {
+ if (principalId !== assessmentSubmission.principal_id) {
+ next(
+ new UnauthorizedError(
+ `Could not access submission with ID ${submissionIdParsed}.`
+ )
+ );
+ return;
+ }
+ }
+
+ const includeQuestionsAndAllAnswers = true;
+ const includeQuestionsAndCorrectAnswers =
+ programRole === 'Facilitator' ||
+ assessmentSubmission.assessment_submission_state === 'Graded';
+
+ const curriculumAssessment = await getCurriculumAssessment(
+ programAssessment.assessment_id,
+ includeQuestionsAndAllAnswers,
+ includeQuestionsAndCorrectAnswers
+ );
+
+ const assessmentWithSubmission: SavedAssessment = {
+ curriculum_assessment: curriculumAssessment,
+ program_assessment: programAssessment,
+ principal_program_role: programRole,
+ submission: assessmentSubmission,
+ };
+
+ res.json(itemEnvelope(assessmentWithSubmission));
+ } catch (err) {
+ next(err);
+ return;
+ }
+});
+
+assessmentsRouter.put('/submissions/:submissionId', async (req, res, next) => {
+ const { principalId } = req.session;
+ const { submissionId } = req.params;
+ const submissionIdParsed = Number(submissionId);
+ const submissionFromUser = req.body;
+
+ try {
+ if (!Number.isInteger(submissionIdParsed) || submissionIdParsed < 1) {
+ throw new BadRequestError(
+ `"${submissionIdParsed}" is not a valid submission ID.`
+ );
+ }
+
+ const isSubmission = (
+ possibleSubmission: unknown
+ ): possibleSubmission is AssessmentSubmission => {
+ return (possibleSubmission as AssessmentSubmission).id !== undefined;
+ };
+
+ if (!isSubmission(submissionFromUser)) {
+ throw new ValidationError(`Was not given a valid assessment submission.`);
+ }
+
+ if (submissionFromUser.id !== submissionIdParsed) {
+ throw new BadRequestError(
+ `The submission ID in the parameter (${submissionIdParsed}) is not the same ID as in the request body (${submissionFromUser.id}).`
+ );
+ }
+
+ const existingAssessmentSubmission = await getAssessmentSubmission(
+ submissionIdParsed,
+ true
+ );
+
+ if (!existingAssessmentSubmission) {
+ throw new NotFoundError(
+ `Could not find submission with ID ${submissionIdParsed}.`
+ );
+ }
+
+ const programAssessment = await findProgramAssessment(
+ submissionFromUser.assessment_id
+ );
+
+ const programRole = await getPrincipalProgramRole(
+ principalId,
+ programAssessment.program_id
+ );
+
+ if (!programRole) {
+ throw new UnauthorizedError(
+ `Could not access the assessment and submssion without enrollment in the program or being a facilitator.`
+ );
+ }
+
+ if (
+ submissionFromUser.principal_id !== principalId &&
+ programRole !== 'Facilitator'
+ ) {
+ throw new UnauthorizedError(
+ `You may not update an assessment that is not your own.`
+ );
+ }
+
+ let updatedSubmission: AssessmentSubmission;
+
+ if (programRole === 'Facilitator') {
+ updatedSubmission = await updateAssessmentSubmission(
+ submissionFromUser,
+ programRole === 'Facilitator'
+ );
+ } else if (
+ ['Opened', 'In Progress'].includes(
+ existingAssessmentSubmission.assessment_submission_state
+ )
+ ) {
+ updatedSubmission = await updateAssessmentSubmission(
+ submissionFromUser,
+ programRole === 'Facilitator'
+ );
+ } else {
+ updatedSubmission = await getAssessmentSubmission(
+ existingAssessmentSubmission.id,
+ true,
+ programRole === 'Facilitator'
+ );
+ }
+
+ res.json(itemEnvelope(updatedSubmission));
+ } catch (err) {
+ next(err);
+ return;
+ }
+});
+
+export default assessmentsRouter;
diff --git a/api/src/middleware/httpErrors.ts b/api/src/middleware/httpErrors.ts
index 72e0e3a5..21f429ab 100644
--- a/api/src/middleware/httpErrors.ts
+++ b/api/src/middleware/httpErrors.ts
@@ -20,6 +20,15 @@ UnauthorizedError.prototype.message =
'The requester does not have access to the resource.';
UnauthorizedError.prototype.status = 401;
+export class ForbiddenError extends HttpError {}
+ForbiddenError.prototype.message = 'Access to this resource is not allowed.';
+ForbiddenError.prototype.status = 403;
+
+export class ConflictError extends HttpError {}
+ConflictError.prototype.message =
+ 'This request is in conflict with the state of the server.';
+ConflictError.prototype.status = 409;
+
export class ValidationError extends HttpError {}
ValidationError.prototype.message =
'The provided data does not meet requirements.';
diff --git a/api/src/middleware/routerTestUtils.ts b/api/src/middleware/routerTestUtils.ts
index 4e1efed5..f38e5c05 100644
--- a/api/src/middleware/routerTestUtils.ts
+++ b/api/src/middleware/routerTestUtils.ts
@@ -4,6 +4,7 @@ import express, { NextFunction, Request, Response, Router } from 'express';
import expressSession from 'express-session';
import { Server, Socket } from 'socket.io';
import { SuperAgent, SuperAgentRequest } from 'superagent';
+import { handleErrors } from './errorHandlingMiddleware';
declare module 'supertest' {
interface SuperTest extends SuperAgent {
@@ -44,5 +45,6 @@ export const createAppAgentForRouter = (router: Router): SuperAgentTest => {
next();
});
app.use(router);
+ app.use(handleErrors(() => null));
return agent(server);
};
diff --git a/api/src/models.d.ts b/api/src/models.d.ts
index 9884876a..aa7f88a8 100644
--- a/api/src/models.d.ts
+++ b/api/src/models.d.ts
@@ -72,3 +72,111 @@ export interface ParticipantActivityForProgram {
program_id: number;
participant_activities: ParticipantActivityCompletionStatus[];
}
+
+export interface ProgramParticipantCompletionSummary {
+ program: Program;
+ principal_id: number;
+ total_score: number;
+}
+
+export interface Answer {
+ id?: number;
+ question_id?: number;
+ title: string;
+ description?: string;
+ sort_order: number;
+ correct_answer?: boolean;
+}
+
+export interface Question {
+ id?: number;
+ assessment_id?: number;
+ title: string;
+ description?: string;
+ question_type: string;
+ answers?: Answer[];
+ correct_answer_id?: number;
+ max_score: number;
+ sort_order: number;
+}
+
+export interface AssessmentResponse {
+ id?: number;
+ assessment_id: number;
+ submission_id: number;
+ question_id: number;
+ answer_id?: number;
+ response_text?: string;
+ score?: number;
+ grader_response?: string;
+}
+
+export interface AssessmentSubmission {
+ id?: number;
+ assessment_id: number;
+ principal_id: number;
+ assessment_submission_state: string;
+ score?: number;
+ opened_at: string;
+ submitted_at?: string;
+ last_modified: string;
+ responses?: AssessmentResponse[];
+}
+
+export interface ParticipantAssessmentSubmissionsSummary {
+ principal_id: number;
+ highest_state: string;
+ total_num_submissions: number;
+ most_recent_submitted_date?: string;
+ highest_score?: number;
+}
+
+export interface FacilitatorAssessmentSubmissionsSummary {
+ num_participants_with_submissions: number;
+ num_program_participants: number;
+ num_ungraded_submissions: number;
+}
+
+export interface CurriculumAssessment {
+ id?: number;
+ title: string;
+ assessment_type: string;
+ description?: string;
+ max_score: number;
+ max_num_submissions: number;
+ time_limit?: number;
+ curriculum_id: number;
+ activity_id: number;
+ principal_id: number;
+ questions?: Question[];
+}
+
+export interface ProgramAssessment {
+ id?: number;
+ program_id: number;
+ assessment_id?: number;
+ available_after: string;
+ due_date: string;
+}
+
+export interface AssessmentDetails {
+ curriculum_assessment: CurriculumAssessment;
+ program_assessment: ProgramAssessment;
+}
+
+interface AssessmentWithRole extends AssessmentDetails {
+ principal_program_role: string;
+}
+
+export interface AssessmentWithSummary extends AssessmentWithRole {
+ participant_submissions_summary?: ParticipantAssessmentSubmissionsSummary;
+ facilitator_submissions_summary?: FacilitatorAssessmentSubmissionsSummary;
+}
+
+export interface SavedAssessment extends AssessmentWithRole {
+ submission: AssessmentSubmission;
+}
+
+export interface AssessmentWithSubmissions extends AssessmentWithRole {
+ submissions: AssessmentSubmission[];
+}
diff --git a/api/src/server.ts b/api/src/server.ts
index f705fdaa..af4057d2 100644
--- a/api/src/server.ts
+++ b/api/src/server.ts
@@ -1,15 +1,16 @@
// istanbul ignore file
/* eslint-disable @typescript-eslint/ban-ts-comment */
-import connectRedis from 'connect-redis';
+import RedisStore from 'connect-redis';
import cors from 'cors';
import { createClient as createRedisClient } from 'redis';
import { createServer } from 'http';
import express from 'express';
-import expressSession, { Session, SessionData } from 'express-session';
+import session, { Session, SessionData } from 'express-session';
import helmet from 'helmet';
import Rollbar from 'rollbar';
import { Server } from 'socket.io';
+import assessmentsRouter from './middleware/assessmentsRouter';
import authRouter from './middleware/authRouter';
import competenciesRouter from './middleware/competenciesRouter';
import docsRouter from './middleware/docsRouter';
@@ -86,35 +87,59 @@ const start = async () => {
});
});
- const RedisStore = connectRedis(expressSession);
+ app.set('trust proxy', 1);
+
+ app.use(helmet());
+ app.use(express.json());
+
+ // Session information is stored in Redis for retrieval by both the
+ // webapp front-end and the api back-end.
+ const redisHost = findConfig('REDIS_HOST', 'localhost');
const redisClient = createRedisClient({
- host: findConfig('REDIS_HOST', 'localhost'),
- });
- redisClient.on('connect', () => console.log(`redis client connected`));
- redisClient.on('error', error => console.log(`redis client error: ${error}`));
-
- const sessionMiddleware = expressSession({
- cookie: { sameSite: true, secure },
- name: 'session_id',
- resave: true,
- saveUninitialized: true,
- secret: findConfig('SESSION_SECRET', ''),
- store: new RedisStore({ client: redisClient }),
+ url: `redis://${redisHost}`,
});
- io.use((socket, next) => {
- // This code was taken from the documentation for using Socket.IO with express-session:
- // https://socket.io/docs/v4/faq/#usage-with-express-session
- // This was not designed with typescript in mind so it shows that the types are incompatible
- // @ts-ignore
- sessionMiddleware(socket.request, {}, next);
- });
+ // Try connecting to Redis. If we can't, notify the user and fall back
+ // to in-memory session store, if possible.
+ try {
+ await redisClient.connect();
+
+ if (!redisClient.isOpen) {
+ throw new Error('trouble connecting to redis.');
+ } else {
+ console.info('redis client connected');
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const sessionStore = new (RedisStore as any)({ client: redisClient });
+
+ const sessionMiddleware = session({
+ cookie: { sameSite: true, secure },
+ name: 'session_id',
+ resave: true,
+ saveUninitialized: true,
+ secret: findConfig('SESSION_SECRET', ''),
+ store: sessionStore,
+ });
- app.set('trust proxy', 1);
+ app.use(sessionMiddleware);
+ } catch (err) {
+ console.error(`redis client error: ${err}`);
+ console.error("Have you run 'docker compose up' yet?");
+
+ const sessionMiddleware = session({
+ cookie: { sameSite: true, secure },
+ name: 'session_id',
+ resave: true,
+ saveUninitialized: true,
+ secret: findConfig('SESSION_SECRET', ''),
+ });
+
+ app.use(sessionMiddleware);
+ }
+
+ redisClient.on('error', console.error);
- app.use(helmet());
- app.use(express.json());
- app.use(sessionMiddleware);
app.use((req, res, next) => {
req.io = io;
next();
@@ -124,6 +149,7 @@ const start = async () => {
origin: findConfig('WEBAPP_ORIGIN', ''),
});
app.use(withCors, authRouter);
+ app.use('/assessments', withCors, loggedIn, assessmentsRouter);
app.use('/competencies', withCors, loggedIn, competenciesRouter);
app.use('/docs', withCors, docsRouter);
app.use('/meetings', withCors, loggedIn, meetingsRouter);
diff --git a/api/src/services/__tests__/assessmentsService.ts b/api/src/services/__tests__/assessmentsService.ts
new file mode 100644
index 00000000..5ac1a685
--- /dev/null
+++ b/api/src/services/__tests__/assessmentsService.ts
@@ -0,0 +1,3110 @@
+import { DateTime, Settings } from 'luxon';
+
+import {
+ Answer,
+ AssessmentResponse,
+ AssessmentSubmission,
+ CurriculumAssessment,
+ FacilitatorAssessmentSubmissionsSummary,
+ ParticipantAssessmentSubmissionsSummary,
+ ProgramAssessment,
+ Question,
+} from '../../models';
+import { mockQuery } from '../mockDb';
+
+import {
+ constructFacilitatorAssessmentSummary,
+ constructParticipantAssessmentSummary,
+ createAssessmentSubmission,
+ createCurriculumAssessment,
+ createProgramAssessment,
+ deleteCurriculumAssessment,
+ deleteProgramAssessment,
+ enrollFacilitator,
+ enrollParticipant,
+ facilitatorProgramIdsMatchingCurriculum,
+ findProgramAssessment,
+ getAssessmentSubmission,
+ getCurriculumAssessment,
+ getPrincipalProgramRole,
+ listAllProgramAssessmentSubmissions,
+ listParticipantProgramAssessmentSubmissions,
+ listPrincipalEnrolledProgramIds,
+ listProgramAssessments,
+ removeGradingInformation,
+ updateAssessmentSubmission,
+ updateCurriculumAssessment,
+ updateProgramAssessment,
+} from '../assessmentsService';
+
+/* EXAMPLE DATA: Variables */
+
+const participantPrincipalId = 30;
+const unenrolledPrincipalId = 31;
+const facilitatorPrincipalId = 300;
+const curriculumAssessmentId = 8;
+const programAssessmentId = 16;
+const singleChoiceQuestionId = 24;
+const freeResponseQuestionId = 24;
+const singleChoiceAnswerId = 28;
+const freeResponseCorrectAnswerId = 29;
+const assessmentSubmissionId = 32;
+const assessmentSubmissionResponseSCId = 320;
+const assessmentSubmissionResponseFRId = 321;
+
+/* EXAMPLE DATA: Database Rows */
+
+const programParticipantRolesRows = [
+ { id: 1, title: 'Facilitator' },
+ { id: 2, title: 'Participant' },
+];
+
+const curriculumAssessmentsRows = [
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ },
+ {
+ id: 9,
+ title: 'New Curriculum Quiz',
+ description: null as string,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ },
+];
+
+const assessmentQuestionsRows = [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null as string,
+ question_type: 'single choice',
+ correct_answer_id: 28,
+ max_score: 1,
+ sort_order: 1,
+ },
+ {
+ id: 24,
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null as string,
+ question_type: 'free response',
+ correct_answer_id: 29,
+ max_score: 1,
+ sort_order: 1,
+ },
+];
+
+const assessmentAnswersRows = [
+ {
+ id: 28,
+ question_id: 24,
+ title: 'A relational database management system',
+ description: null as string,
+ sort_order: 1,
+ correct_answer: true,
+ },
+ {
+ id: 29,
+ question_id: 24,
+ title: 'Hello, World!
',
+ description: null as string,
+ sort_order: 1,
+ correct_answer: true,
+ },
+ {
+ id: 28,
+ question_id: 24,
+ title: 'A relational database management system',
+ description: 'Also known as a DBMS.',
+ sort_order: 1,
+ correct_answer: true,
+ },
+];
+
+const programsRows = [
+ {
+ id: 12,
+ title: 'Cohort 4',
+ start_date: '2022-10-24',
+ end_date: '2022-12-16',
+ time_zone: 'America/Vancouver',
+ curriculum_id: 4,
+ },
+];
+
+const programAssessmentsRows = [
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2050-06-24 00:00:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2023-02-10 00:00:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2050-06-24 00:00:00',
+ due_date: '2050-06-23 00:00:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2050-06-26 00:00:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2050-06-24 00:00:00',
+ },
+];
+
+const assessmentSubmissionsRows = [
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09 12:00:00',
+ submitted_at: null as string,
+ updated_at: '2023-02-09 12:00:00',
+ score: null as number,
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09 12:00:00',
+ submitted_at: null as string,
+ updated_at: '2023-02-09 12:00:00',
+ score: null as number,
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Expired',
+ opened_at: '2023-02-09 12:00:00',
+ submitted_at: null as string,
+ updated_at: '2023-02-09 14:00:00',
+ score: null as number,
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09 12:00:00',
+ submitted_at: '2023-02-09 13:23:45',
+ updated_at: '2023-02-09 13:23:45',
+ score: null as number,
+ },
+ {
+ id: 36,
+ assessment_id: 16,
+ principal_id: 32,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09 12:01:00',
+ last_modified: '2023-02-09 12:01:00',
+ submitted_at: '2023-02-09 13:23:45',
+ updated_at: '2023-02-09 13:23:45',
+ score: null as number,
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Graded',
+ opened_at: '2023-02-09 12:00:00',
+ submitted_at: '2023-02-09 13:23:45',
+ updated_at: '2023-02-09 13:23:45',
+ score: 4,
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09 12:00:00',
+ submitted_at: '2023-02-09 13:23:45',
+ updated_at: '2023-02-09 12:00:00',
+ score: null as number,
+ last_modified: '2023-02-09 13:23:45',
+ },
+];
+
+const assessmentResponsesRows = [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: null as number,
+ response: null as string,
+ score: null as number,
+ grader_response: null as string,
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ response: null as string,
+ score: null as number,
+ grader_response: null as string,
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ response: null as string,
+ score: null as number,
+ grader_response: null as string,
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ response: null as string,
+ score: 1,
+ grader_response: 'Well done!',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: null as number,
+ response: null as string,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: null as number,
+ response: 'Hello world!
',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: null as number,
+ response: 'Hello world!
',
+ score: 0,
+ grader_response: 'Very close!',
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ score: null as number,
+ grader_response: 'Well done!',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response: 'Hello world!
',
+ score: 0,
+ grader_response: 'Very close!',
+ },
+];
+
+/* EXAMPLE DATA: Structured Data */
+
+const curriculumAssessments: CurriculumAssessment[] = [
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 3,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null,
+ question_type: 'free response',
+ answers: [
+ {
+ id: 29,
+ question_id: 24,
+ description: null,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 29,
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null as string,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ questions: [
+ {
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ answers: [
+ {
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ questions: [
+ {
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null,
+ question_type: 'free response',
+ answers: [
+ {
+ description: null,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 121,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ answers: [
+ {
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ answers: [
+ {
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 8,
+ title: 'Assignment 1: React',
+ assessment_type: 'test',
+ description: 'Your assignment for week 1 learning.',
+ max_score: 10,
+ max_num_submissions: 1,
+ time_limit: 120,
+ curriculum_id: 4,
+ activity_id: 20,
+ principal_id: 3,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: 'Also known as a DBMS.',
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null as string,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ id: 9,
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ id: 9,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ ],
+ },
+ {
+ title: 'New Curriculum Quiz',
+ assessment_type: 'quiz',
+ description: null,
+ max_score: 42,
+ max_num_submissions: 13,
+ time_limit: 60,
+ curriculum_id: 4,
+ activity_id: 200,
+ principal_id: 300,
+ id: 9,
+ questions: [
+ {
+ id: 24,
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null,
+ question_type: 'free response',
+ answers: [
+ {
+ id: 29,
+ question_id: 24,
+ description: null,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 29,
+ max_score: 1,
+ sort_order: 1,
+ },
+ ],
+ },
+];
+
+const answers: Answer[] = [
+ {
+ id: 28,
+ question_id: 24,
+ description: null as string,
+ title: 'A relational database management system',
+ sort_order: 1,
+ },
+ {
+ id: 28,
+ question_id: 24,
+ description: null as string,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ {
+ id: 29,
+ question_id: 24,
+ description: null as string,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ },
+ {
+ id: 29,
+ question_id: 24,
+ description: null as string,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ {
+ description: null as string,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ {
+ description: null as string,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ {
+ id: 28,
+ question_id: 24,
+ description: 'Also known as a DBMS.',
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+];
+
+const questions: Question[] = [
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null as string,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ },
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null as string,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null as string,
+ title: 'A relational database management system',
+ sort_order: 1,
+ },
+ ],
+ },
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null as string,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: null as string,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+ {
+ id: 24,
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null as string,
+ question_type: 'free response',
+ answers: [
+ {
+ id: 29,
+ question_id: 24,
+ description: null as string,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 29,
+ max_score: 1,
+ sort_order: 1,
+ },
+ {
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null as string,
+ question_type: 'single choice',
+ answers: [
+ {
+ description: null as string,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ {
+ assessment_id: 8,
+ title:
+ 'What is the correct HTML syntax for a paragraph with the text "Hello, World!"?',
+ description: null as string,
+ question_type: 'free response',
+ answers: [
+ {
+ description: null as string,
+ title: 'Hello, World!
',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ max_score: 1,
+ sort_order: 1,
+ },
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null as string,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ description: null as string,
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ },
+ {
+ id: 24,
+ assessment_id: 8,
+ title: 'What is React?',
+ description: null as string,
+ question_type: 'single choice',
+ max_score: 1,
+ sort_order: 1,
+ answers: [
+ {
+ id: 28,
+ question_id: 24,
+ description: 'Also known as a DBMS.',
+ title: 'A relational database management system',
+ sort_order: 1,
+ correct_answer: true,
+ },
+ ],
+ correct_answer_id: 28,
+ },
+];
+
+const programAssessments: ProgramAssessment[] = [
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2050-06-24T00:00:00.000-07:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06T00:00:00.000-08:00',
+ due_date: '2023-02-10T00:00:00.000-08:00',
+ },
+ {
+ id: 16,
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2050-06-24T00:00:00.000-07:00',
+ due_date: '2050-06-23T00:00:00.000-07:00',
+ },
+ {
+ program_id: 12,
+ assessment_id: 8,
+ available_after: '2023-02-06 00:00:00',
+ due_date: '2050-06-24 00:00:00',
+ },
+];
+
+const participantSummaries: ParticipantAssessmentSubmissionsSummary[] = [
+ {
+ principal_id: 30,
+ highest_state: 'Inactive',
+ total_num_submissions: 0,
+ },
+ {
+ principal_id: 30,
+ highest_state: 'Expired',
+ total_num_submissions: 1,
+ },
+ {
+ principal_id: 30,
+ highest_state: 'Active',
+ total_num_submissions: 0,
+ },
+ {
+ principal_id: 30,
+ highest_state: 'Graded',
+ most_recent_submitted_date: '2023-02-09T13:23:45.000Z',
+ total_num_submissions: 1,
+ highest_score: 4,
+ },
+];
+
+const facilitatorSummaries: FacilitatorAssessmentSubmissionsSummary[] = [
+ {
+ num_participants_with_submissions: 8,
+ num_program_participants: 12,
+ num_ungraded_submissions: 6,
+ },
+];
+
+const assessmentResponses: AssessmentResponse[] = [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ score: 1,
+ grader_response: 'Well done!',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ score: 0,
+ grader_response: 'Very close!',
+ },
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ id: 320,
+ score: null as number,
+ grader_response: null as string,
+ },
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ id: 321,
+ score: null as number,
+ grader_response: null as string,
+ },
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ id: 320,
+ score: 1,
+ grader_response: 'Well done!',
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ score: 0,
+ grader_response: 'Very close!',
+ },
+];
+
+const assessmentSubmissions: AssessmentSubmission[] = [
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:00:00.000Z',
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:00:00.000Z',
+ responses: [
+ { id: 320, assessment_id: 16, submission_id: 32, question_id: 24 },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'In Progress',
+ opened_at: '2023-02-10T07:00:00.000Z',
+ last_modified: '2023-02-10T07:05:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Expired',
+ opened_at: '2023-02-10T07:00:00.000Z',
+ last_modified: '2023-02-17T08:00:10.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Expired',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T14:00:00.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Expired',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-16T14:00:10.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ id: 321,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 36,
+ assessment_id: 16,
+ principal_id: 32,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:01:00.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Graded',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ score: 4,
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Graded',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ score: 4,
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ score: 1,
+ grader_response: 'Well done!',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Graded',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ id: 320,
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Opened',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-09T12:05:00.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-10T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ id: 320,
+ score: null as number,
+ grader_response: null as string,
+ },
+ ],
+ },
+ {
+ id: 32,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-10T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ response_text: 'Hello world!
',
+ id: 321,
+ score: null as number,
+ grader_response: null as string,
+ },
+ ],
+ },
+ {
+ id: 33,
+ assessment_id: 16,
+ principal_id: 30,
+ assessment_submission_state: 'Submitted',
+ opened_at: '2023-02-09T12:00:00.000Z',
+ last_modified: '2023-02-10T13:23:45.000Z',
+ submitted_at: '2023-02-09T13:23:45.000Z',
+ responses: [
+ {
+ assessment_id: 16,
+ submission_id: 32,
+ question_id: 24,
+ answer_id: 28,
+ id: 320,
+ score: null as number,
+ grader_response: null as string,
+ },
+ ],
+ },
+];
+
+describe('constructFacilitatorAssessmentSummary', () => {
+ it('should gather the relevant information for constructing a FacilitatorAssessmentSubmissionsSummary for a given program assessment', async () => {
+ mockQuery(
+ 'select count(distinct `principal_id`) as `count` from `assessment_submissions` where `assessment_id` = ?',
+ [programAssessments[0].id],
+ [
+ {
+ count: facilitatorSummaries[0].num_participants_with_submissions,
+ },
+ ]
+ );
+ mockQuery(
+ 'select count(`id`) as `count` from `program_participants` where `program_id` = ? and `role_id` = ?',
+ [programAssessments[0].program_id, 2],
+ [
+ {
+ count: facilitatorSummaries[0].num_program_participants,
+ },
+ ]
+ );
+ mockQuery(
+ 'select count(`id`) as `count` from `assessment_submissions` where `assessment_id` = ? and `score` is null',
+ [programAssessments[0].id],
+ [
+ {
+ count: facilitatorSummaries[0].num_ungraded_submissions,
+ },
+ ]
+ );
+
+ expect(
+ await constructFacilitatorAssessmentSummary(programAssessments[0])
+ ).toEqual(facilitatorSummaries[0]);
+ });
+});
+
+describe('constructParticipantAssessmentSummary', () => {
+ it('should gather the relevant information for constructing a ParticipantAssessmentSubmissionsSummary for a given program assessment', async () => {
+ mockQuery(
+ 'select `assessment_submission_states`.`title` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submission_states`.`id` = `assessment_submissions`.`assessment_submission_state_id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ? order by `assessment_submissions`.`assessment_submission_state_id` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ [
+ {
+ title: assessmentSubmissionsRows[5].assessment_submission_state,
+ },
+ ]
+ );
+ mockQuery(
+ 'select `submitted_at` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `submitted_at` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ [
+ {
+ submitted_at: assessmentSubmissionsRows[5].submitted_at,
+ },
+ ]
+ );
+ mockQuery(
+ 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?',
+ [participantPrincipalId, programAssessments[0].id],
+ [assessmentSubmissionsRows[5]]
+ );
+ mockQuery(
+ 'select `score` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `score` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ [{ score: assessmentSubmissionsRows[5].score }]
+ );
+
+ expect(
+ await constructParticipantAssessmentSummary(
+ participantPrincipalId,
+ programAssessments[0]
+ )
+ ).toEqual(participantSummaries[3]);
+ });
+
+ it('should gather the relevant information for constructing a ParticipantAssessmentSubmissionsSummary even if no submissions, before assessment is active', async () => {
+ mockQuery(
+ 'select `assessment_submission_states`.`title` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submission_states`.`id` = `assessment_submissions`.`assessment_submission_state_id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ? order by `assessment_submissions`.`assessment_submission_state_id` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+ mockQuery(
+ 'select `submitted_at` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `submitted_at` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+
+ mockQuery(
+ 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?',
+ [participantPrincipalId, programAssessments[0].id],
+ []
+ );
+ mockQuery(
+ 'select `score` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `score` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+
+ expect(
+ await constructParticipantAssessmentSummary(
+ participantPrincipalId,
+ programAssessments[2]
+ )
+ ).toEqual(participantSummaries[0]);
+ });
+
+ it('should gather the relevant information for constructing a ParticipantAssessmentSubmissionsSummary even if no submissions, for an active assessment', async () => {
+ mockQuery(
+ 'select `assessment_submission_states`.`title` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submission_states`.`id` = `assessment_submissions`.`assessment_submission_state_id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ? order by `assessment_submissions`.`assessment_submission_state_id` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+ mockQuery(
+ 'select `submitted_at` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `submitted_at` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+ mockQuery(
+ 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?',
+ [participantPrincipalId, programAssessments[0].id],
+ []
+ );
+ mockQuery(
+ 'select `score` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `score` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+
+ expect(
+ await constructParticipantAssessmentSummary(
+ participantPrincipalId,
+ programAssessments[0]
+ )
+ ).toEqual(participantSummaries[2]);
+ });
+
+ it('should gather the relevant information for constructing a ParticipantAssessmentSubmissionsSummary even if no submissions, after assessment is due', async () => {
+ mockQuery(
+ 'select `assessment_submission_states`.`title` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submission_states`.`id` = `assessment_submissions`.`assessment_submission_state_id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ? order by `assessment_submissions`.`assessment_submission_state_id` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+ mockQuery(
+ 'select `submitted_at` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `submitted_at` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+ mockQuery(
+ 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?',
+ [participantPrincipalId, programAssessments[0].id],
+ [1]
+ );
+ mockQuery(
+ 'select `score` from `assessment_submissions` where `principal_id` = ? and `assessment_id` = ? order by `score` desc limit ?',
+ [participantPrincipalId, programAssessments[0].id, 1],
+ []
+ );
+
+ expect(
+ await constructParticipantAssessmentSummary(
+ participantPrincipalId,
+ programAssessments[1]
+ )
+ ).toEqual(participantSummaries[1]);
+ });
+});
+
+describe('createAssessmentSubmission', () => {
+ it('should create a new AssessmentSubmission for a program assessment', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 12, 0, 0);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['Opened'],
+ [{ id: 3 }]
+ );
+ mockQuery(
+ 'insert into `assessment_submissions` (`assessment_id`, `assessment_submission_state_id`, `principal_id`) values (?, ?, ?)',
+ [programAssessments[0].id, 3, participantPrincipalId],
+ [assessmentSubmissions[1].id]
+ );
+ mockQuery(
+ 'select `id` from `assessment_questions` where `assessment_id` = ?',
+ [programAssessments[0].assessment_id],
+ [{ id: singleChoiceQuestionId }]
+ );
+ mockQuery(
+ 'insert into `assessment_responses` (`answer_id`, `assessment_id`, `question_id`, `response`, `submission_id`) values (DEFAULT, ?, ?, DEFAULT, ?)',
+ [
+ programAssessments[0].id,
+ singleChoiceQuestionId,
+ assessmentSubmissions[1].id,
+ ],
+ [assessmentSubmissionResponseSCId]
+ );
+
+ expect(
+ await createAssessmentSubmission(
+ participantPrincipalId,
+ programAssessments[0].id,
+ programAssessments[0].assessment_id
+ )
+ ).toEqual(assessmentSubmissions[1]);
+ });
+});
+
+describe('createCurriculumAssessment', () => {
+ it('should create a curriculum assessment ID without question', async () => {
+ mockQuery(
+ 'insert into `curriculum_assessments` (`activity_id`, `curriculum_id`, `description`, `max_num_submissions`, `max_score`, `principal_id`, `time_limit`, `title`) values (?, ?, ?, ?, ?, ?, ?, ?)',
+ [
+ curriculumAssessments[5].activity_id,
+ curriculumAssessments[5].curriculum_id,
+ curriculumAssessments[5].description,
+ curriculumAssessments[5].max_num_submissions,
+ curriculumAssessments[5].max_score,
+ curriculumAssessments[5].principal_id,
+ curriculumAssessments[5].time_limit,
+ curriculumAssessments[5].title,
+ ],
+ [curriculumAssessments[13].id]
+ );
+ expect(await createCurriculumAssessment(curriculumAssessments[5])).toEqual(
+ curriculumAssessments[13]
+ );
+ });
+
+ it('should create a curriculum assessment ID with a single choice question', async () => {
+ mockQuery(
+ 'insert into `curriculum_assessments` (`activity_id`, `curriculum_id`, `description`, `max_num_submissions`, `max_score`, `principal_id`, `time_limit`, `title`) values (?, ?, ?, ?, ?, ?, ?, ?)',
+ [
+ curriculumAssessments[6].activity_id,
+ curriculumAssessments[6].curriculum_id,
+ curriculumAssessments[6].description,
+ curriculumAssessments[6].max_num_submissions,
+ curriculumAssessments[6].max_score,
+ curriculumAssessments[6].principal_id,
+ curriculumAssessments[6].time_limit,
+ curriculumAssessments[6].title,
+ ],
+ [curriculumAssessments[14].id]
+ );
+ mockQuery(
+ 'insert into `assessment_questions` (`assessment_id`, `description`, `max_score`, `question_type_id`, `sort_order`, `title`) values (?, ?, ?, ?, ?, ?)',
+ [
+ curriculumAssessments[14].id,
+ questions[4].description,
+ questions[4].max_score,
+ 1,
+ questions[4].sort_order,
+ questions[4].title,
+ ],
+ [singleChoiceQuestionId]
+ );
+ mockQuery(
+ 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)',
+ [
+ answers[4].description,
+ singleChoiceQuestionId,
+ answers[4].sort_order,
+ answers[4].title,
+ ],
+ [singleChoiceAnswerId]
+ );
+ mockQuery(
+ 'update `assessment_questions` set `correct_answer_id` = ? where `id` = ?',
+ [singleChoiceAnswerId, singleChoiceQuestionId],
+ []
+ );
+ expect(await createCurriculumAssessment(curriculumAssessments[6])).toEqual(
+ curriculumAssessments[14]
+ );
+ });
+
+ it('should create a curriculum assessment ID with a free response question', async () => {
+ mockQuery(
+ 'insert into `curriculum_assessments` (`activity_id`, `curriculum_id`, `description`, `max_num_submissions`, `max_score`, `principal_id`, `time_limit`, `title`) values (?, ?, ?, ?, ?, ?, ?, ?)',
+ [
+ curriculumAssessments[7].activity_id,
+ curriculumAssessments[7].curriculum_id,
+ curriculumAssessments[7].description,
+ curriculumAssessments[7].max_num_submissions,
+ curriculumAssessments[7].max_score,
+ curriculumAssessments[7].principal_id,
+ curriculumAssessments[7].time_limit,
+ curriculumAssessments[7].title,
+ ],
+ [curriculumAssessments[15].id]
+ );
+ mockQuery(
+ 'insert into `assessment_questions` (`assessment_id`, `description`, `max_score`, `question_type_id`, `sort_order`, `title`) values (?, ?, ?, ?, ?, ?)',
+ [
+ curriculumAssessments[15].id,
+ questions[5].description,
+ questions[5].max_score,
+ 2,
+ questions[5].sort_order,
+ questions[5].title,
+ ],
+ [freeResponseQuestionId]
+ );
+ mockQuery(
+ 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)',
+ [
+ answers[5].description,
+ freeResponseQuestionId,
+ answers[5].sort_order,
+ answers[5].title,
+ ],
+ [freeResponseCorrectAnswerId]
+ );
+ mockQuery(
+ 'update `assessment_questions` set `correct_answer_id` = ? where `id` = ?',
+ [freeResponseCorrectAnswerId, freeResponseQuestionId],
+ []
+ );
+ expect(await createCurriculumAssessment(curriculumAssessments[7])).toEqual(
+ curriculumAssessments[15]
+ );
+ });
+});
+
+describe('createProgramAssessment', () => {
+ it('should insert a ProgramAssessment into the database', async () => {
+ mockQuery(
+ 'insert into `program_assessments` (`assessment_id`, `available_after`, `due_date`, `program_id`) values (?, ?, ?, ?)',
+ [
+ programAssessments[3].assessment_id,
+ programAssessments[3].available_after,
+ programAssessments[3].due_date,
+ programAssessments[3].program_id,
+ ],
+ [programAssessmentsRows[4].id]
+ );
+
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[3].program_id],
+ [programsRows[0]]
+ );
+
+ expect(await createProgramAssessment(programAssessments[3])).toEqual(
+ programAssessments[0]
+ );
+ });
+});
+
+describe('deleteCurriculumAssessment', () => {
+ it('should delete a CurriculumAssessment from the database', async () => {
+ mockQuery(
+ 'delete from `curriculum_assessments` where `id` = ?',
+ [curriculumAssessmentId],
+ [1]
+ );
+
+ expect(await deleteCurriculumAssessment(curriculumAssessmentId)).toEqual([
+ 1,
+ ]);
+ });
+});
+
+describe('deleteProgramAssessment', () => {
+ it('should delete a ProgramAssessment from the database', async () => {
+ mockQuery(
+ 'delete from `program_assessments` where `id` = ?',
+ [programAssessmentId],
+ [1]
+ );
+
+ expect(await deleteProgramAssessment(programAssessmentId)).toEqual([1]);
+ });
+});
+
+describe('enrollFacilitator', () => {
+ it('should enroll a principal ID into a program as a facilitator', async () => {
+ mockQuery(
+ 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?',
+ [facilitatorPrincipalId, programAssessments[0].program_id],
+ []
+ );
+ mockQuery(
+ 'insert into `program_participants` (`principal_id`, `program_id`, `role_id`) values (?, ?, ?)',
+ [facilitatorPrincipalId, programAssessments[0].program_id, 1],
+ [1]
+ );
+
+ expect(
+ await enrollFacilitator(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual(true);
+ });
+
+ it('should switch a participant role ID into a facilitator for a given program', async () => {
+ mockQuery(
+ 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?',
+ [facilitatorPrincipalId, programAssessments[0].program_id],
+ [{ role_id: 2 }]
+ );
+ mockQuery(
+ 'update `program_participants` set `role_id` = ? where `principal_id` = ? and `program_id` = ?',
+ [1, facilitatorPrincipalId, programAssessments[0].program_id],
+ [1]
+ );
+
+ expect(
+ await enrollFacilitator(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual(true);
+ });
+
+ it('should do nothing if the user is already a facilitator of that program', async () => {
+ mockQuery(
+ 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?',
+ [facilitatorPrincipalId, programAssessments[0].program_id],
+ [{ role_id: 1 }]
+ );
+
+ expect(
+ await enrollFacilitator(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual(false);
+ });
+});
+
+describe('enrollParticipant', () => {
+ it('should enroll a principal ID into a program as a participant', async () => {
+ mockQuery(
+ 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?',
+ [participantPrincipalId, programAssessments[0].program_id],
+ []
+ );
+ mockQuery(
+ 'insert into `program_participants` (`principal_id`, `program_id`, `role_id`) values (?, ?, ?)',
+ [participantPrincipalId, programAssessments[0].program_id, 2],
+ [1]
+ );
+
+ expect(
+ await enrollParticipant(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual(true);
+ });
+
+ it('should switch a facilitator role ID into a participant for a given program', async () => {
+ mockQuery(
+ 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?',
+ [participantPrincipalId, programAssessments[0].program_id],
+ [{ role_id: 1 }]
+ );
+ mockQuery(
+ 'update `program_participants` set `role_id` = ? where `principal_id` = ? and `program_id` = ?',
+ [2, participantPrincipalId, programAssessments[0].program_id],
+ [1]
+ );
+
+ expect(
+ await enrollParticipant(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual(true);
+ });
+
+ it('should do nothing if the user is already a facilitator of that program', async () => {
+ mockQuery(
+ 'select `role_id` from `program_participants` where `principal_id` = ? and `program_id` = ?',
+ [participantPrincipalId, programAssessments[0].program_id],
+ [{ role_id: 2 }]
+ );
+
+ expect(
+ await enrollParticipant(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual(false);
+ });
+});
+
+describe('facilitatorProgramIdsMatchingCurriculum', () => {
+ it('should return an array of program IDs for a principal that is facilitator of at least one program', async () => {
+ mockQuery(
+ 'select `program_id` from `program_participants` where `principal_id` = ?',
+ [facilitatorPrincipalId],
+ [{ program_id: programAssessments[0].program_id }]
+ );
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `curriculum_id` = ?',
+ [curriculumAssessments[0].curriculum_id],
+ [programsRows[0]]
+ );
+ mockQuery(
+ 'select `program_participant_roles`.`title` from `program_participant_roles` inner join `program_participants` on `program_participant_roles`.`id` = `program_participants`.`role_id` where `principal_id` = ? and `program_id` = ?',
+ [facilitatorPrincipalId, programAssessments[0].program_id],
+ [{ title: 'Facilitator' }]
+ );
+
+ expect(
+ await facilitatorProgramIdsMatchingCurriculum(
+ facilitatorPrincipalId,
+ curriculumAssessments[0].curriculum_id
+ )
+ ).toEqual([programAssessments[0].program_id]);
+ });
+
+ it('should return an empty array of program IDs for a principal that is not a facilitator of at least one program', async () => {
+ mockQuery(
+ 'select `program_id` from `program_participants` where `principal_id` = ?',
+ [participantPrincipalId],
+ []
+ );
+ expect(
+ await facilitatorProgramIdsMatchingCurriculum(
+ participantPrincipalId,
+ curriculumAssessments[0].curriculum_id
+ )
+ ).toEqual([]);
+ });
+});
+
+describe('findProgramAssessment', () => {
+ it('should return a ProgramAssessment for an existing program assessment ID', async () => {
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[0].id],
+ [programAssessmentsRows[0]]
+ );
+
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[0].program_id],
+ [programsRows[0]]
+ );
+
+ expect(await findProgramAssessment(programAssessments[0].id)).toEqual(
+ programAssessments[0]
+ );
+ });
+
+ it('should return null for a program assessment ID that does not exist', async () => {
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[0].id],
+ []
+ );
+
+ expect(await findProgramAssessment(programAssessments[0].id)).toEqual(null);
+ });
+});
+
+describe('getAssessmentSubmission', () => {
+ it('should get assessment submission based on given submission ID', async () => {
+ const assessmentSubmissionId = assessmentSubmissions[13].id;
+ const responsesIncluded = true;
+ const gradingsIncluded = true;
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[5]]
+ );
+
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[3]]
+ );
+
+ expect(
+ await getAssessmentSubmission(
+ assessmentSubmissionId,
+ responsesIncluded,
+ gradingsIncluded
+ )
+ ).toEqual(assessmentSubmissions[13]);
+ });
+
+ it('should get assessment submission with null for responses (if no responses found) based on given submission ID', async () => {
+ const assessmentSubmissionId = assessmentSubmissionsRows[0].id;
+ const responsesIncluded = true;
+ const gradingsIncluded = true;
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[0]]
+ );
+
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ []
+ );
+
+ expect(
+ await getAssessmentSubmission(
+ assessmentSubmissionId,
+ responsesIncluded,
+ gradingsIncluded
+ )
+ ).toEqual(assessmentSubmissions[0]);
+ });
+
+ it('should return null for a assessment submission ID that does not exist', async () => {
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissions[13].id],
+ []
+ );
+
+ expect(
+ await getAssessmentSubmission(assessmentSubmissions[13].id, true, true)
+ ).toEqual(null);
+ });
+});
+
+describe('getCurriculumAssessment', () => {
+ it('should return a CurriculumAssessment for an existing curriculum assessment ID', async () => {
+ const questionsAndAllAnswersIncluded = true,
+ questionsAndCorrectAnswersIncluded = true;
+
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ [curriculumAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?',
+ [curriculumAssessmentsRows[0].activity_id],
+ [
+ {
+ title: curriculumAssessments[3].assessment_type,
+ },
+ ]
+ );
+ mockQuery(
+ 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc',
+ [curriculumAssessments[3].id],
+ [assessmentQuestionsRows[0]]
+ );
+
+ const questionIds = [assessmentQuestionsRows[0].id];
+
+ mockQuery(
+ 'select `id`, `question_id`, `title`, `description`, `sort_order` from `assessment_answers` where `question_id` = ? order by `sort_order` asc',
+ [questionIds[0]],
+ [assessmentAnswersRows[0]]
+ );
+
+ expect(
+ await getCurriculumAssessment(
+ curriculumAssessments[3].id,
+ questionsAndAllAnswersIncluded,
+ questionsAndCorrectAnswersIncluded
+ )
+ ).toEqual(curriculumAssessments[3]);
+ });
+
+ it('should return null for a curriculum assessment ID that does not exist', async () => {
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ []
+ );
+
+ expect(
+ await getCurriculumAssessment(curriculumAssessmentId, true, true)
+ ).toEqual(null);
+ });
+});
+
+describe('getPrincipalProgramRole', () => {
+ it('should return the correct role for a facilitator based on principal ID and program ID', async () => {
+ mockQuery(
+ 'select `program_participant_roles`.`title` from `program_participant_roles` inner join `program_participants` on `program_participant_roles`.`id` = `program_participants`.`role_id` where `principal_id` = ? and `program_id` = ?',
+ [facilitatorPrincipalId, programAssessments[0].program_id],
+ [programParticipantRolesRows[0]]
+ );
+
+ expect(
+ await getPrincipalProgramRole(
+ facilitatorPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual('Facilitator');
+ });
+
+ it('should return the correct role for a participant based on principal ID and program ID', async () => {
+ mockQuery(
+ 'select `program_participant_roles`.`title` from `program_participant_roles` inner join `program_participants` on `program_participant_roles`.`id` = `program_participants`.`role_id` where `principal_id` = ? and `program_id` = ?',
+ [participantPrincipalId, programAssessments[0].program_id],
+ [programParticipantRolesRows[1]]
+ );
+
+ expect(
+ await getPrincipalProgramRole(
+ participantPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual('Participant');
+ });
+
+ it('should return null for a user not enrolled in the program', async () => {
+ mockQuery(
+ 'select `program_participant_roles`.`title` from `program_participant_roles` inner join `program_participants` on `program_participant_roles`.`id` = `program_participants`.`role_id` where `principal_id` = ? and `program_id` = ?',
+ [unenrolledPrincipalId, programAssessments[0].program_id],
+ []
+ );
+
+ expect(
+ await getPrincipalProgramRole(
+ unenrolledPrincipalId,
+ programAssessments[0].program_id
+ )
+ ).toEqual(null);
+ });
+});
+
+describe('listAllProgramAssessmentSubmissions', () => {
+ it('should return all program assessment submissions for a given program assessment', async () => {
+ mockQuery(
+ 'select `assessment_submissions`.`id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`principal_id`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_id` = ?',
+ [assessmentSubmissions[0].assessment_id],
+ [
+ assessmentSubmissionsRows[0],
+ assessmentSubmissionsRows[4],
+ assessmentSubmissionsRows[5],
+ ]
+ );
+
+ expect(
+ await listAllProgramAssessmentSubmissions(
+ assessmentSubmissions[0].assessment_id
+ )
+ ).toEqual([
+ assessmentSubmissions[0],
+ assessmentSubmissions[11],
+ assessmentSubmissions[12],
+ ]);
+ });
+
+ it('should return null if no program assessment submissions for a given program assessment', async () => {
+ mockQuery(
+ 'select `assessment_submissions`.`id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`principal_id`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_id` = ?',
+ [assessmentSubmissions[0].assessment_id],
+ []
+ );
+
+ expect(
+ await listAllProgramAssessmentSubmissions(
+ assessmentSubmissions[0].assessment_id
+ )
+ ).toEqual(null);
+ });
+});
+
+describe('listParticipantProgramAssessmentSubmissions', () => {
+ it('should return program assessment submissions for a participant for a given program assessment', async () => {
+ mockQuery(
+ 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?',
+ [participantPrincipalId, assessmentSubmissions[0].assessment_id],
+ [assessmentSubmissionsRows[0]]
+ );
+ expect(
+ await listParticipantProgramAssessmentSubmissions(
+ participantPrincipalId,
+ assessmentSubmissions[0].assessment_id
+ )
+ ).toEqual([assessmentSubmissions[0]]);
+ });
+
+ it('should return null if no program assessment submissions for a given program assessment', async () => {
+ mockQuery(
+ 'select `assessment_submissions`.`id` as `id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`principal_id` = ? and `assessment_submissions`.`assessment_id` = ?',
+ [participantPrincipalId, assessmentSubmissions[0].assessment_id],
+ []
+ );
+
+ expect(
+ await listParticipantProgramAssessmentSubmissions(
+ participantPrincipalId,
+ assessmentSubmissions[0].assessment_id
+ )
+ ).toEqual(null);
+ });
+});
+
+describe('listPrincipalEnrolledProgramIds', () => {
+ const enrolledProgramsList = [{ program_id: 2 }];
+
+ it('should return program ID list for which a principal is facilitator', async () => {
+ mockQuery(
+ 'select `program_id` from `program_participants` where `principal_id` = ?',
+ [facilitatorPrincipalId],
+ enrolledProgramsList
+ );
+ expect(
+ await listPrincipalEnrolledProgramIds(facilitatorPrincipalId)
+ ).toEqual([enrolledProgramsList[0].program_id]);
+ });
+
+ it('should return program ID list for which a principal is participant', async () => {
+ mockQuery(
+ 'select `program_id` from `program_participants` where `principal_id` = ?',
+ [participantPrincipalId],
+ enrolledProgramsList
+ );
+ expect(
+ await listPrincipalEnrolledProgramIds(participantPrincipalId)
+ ).toEqual([enrolledProgramsList[0].program_id]);
+ });
+});
+
+describe('listProgramAssessments', () => {
+ it('should return all ProgramAssessments linked to a program ID', async () => {
+ mockQuery(
+ 'select `id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `program_id` = ?',
+ [programAssessmentsRows[0].program_id],
+ [programAssessmentsRows[0]]
+ );
+
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessmentsRows[0].program_id],
+ [programsRows[0]]
+ );
+
+ expect(
+ await listProgramAssessments(programAssessmentsRows[0].program_id)
+ ).toEqual([programAssessments[0]]);
+ });
+
+ it('should return null if no ProgramAssessments linked to a program ID were found', async () => {
+ mockQuery(
+ 'select `id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `program_id` = ?',
+ [programAssessmentsRows[0].program_id],
+ []
+ );
+
+ expect(
+ await listProgramAssessments(programAssessmentsRows[0].program_id)
+ ).toEqual(null);
+ });
+});
+
+describe('removeGradingInformation', () => {
+ it('should remove all grading-related information from an AssessmentSubmission', () => {
+ expect(removeGradingInformation(assessmentSubmissions[13])).toEqual(
+ assessmentSubmissions[14]
+ );
+ });
+});
+
+describe('updateAssessmentSubmission', () => {
+ it('should update an existing in-progress assessment submission by a participant with a changed response', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[1]]
+ );
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[0], assessmentResponsesRows[4]]
+ );
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[0].id],
+ [programAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[0].program_id],
+ [programsRows[0]]
+ );
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ [curriculumAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?',
+ [curriculumAssessmentsRows[0].activity_id],
+ [
+ {
+ title: curriculumAssessments[3].assessment_type,
+ },
+ ]
+ );
+ mockQuery(
+ 'update `assessment_responses` set `answer_id` = ? where `id` = ?',
+ [assessmentResponsesRows[1].answer_id, assessmentResponsesRows[1].id],
+ 1
+ );
+ mockQuery(
+ 'update `assessment_responses` set `response` = ? where `id` = ?',
+ [assessmentResponsesRows[5].response, assessmentResponsesRows[5].id],
+ 1
+ );
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['In Progress'],
+ [{ id: 4 }]
+ );
+ mockQuery(
+ 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?',
+ [4, assessmentSubmissions[17].id],
+ 1
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[4], false)
+ ).toEqual(assessmentSubmissions[4]);
+ });
+
+ it('should update an existing submitted assessment submission by adding grading information from a facilitator', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[3]]
+ );
+
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[1]]
+ );
+
+ mockQuery(
+ 'update `assessment_responses` set `score` = ?, `grader_response` = ? where `id` = ?',
+ [
+ assessmentResponsesRows[3].score,
+ assessmentResponsesRows[3].grader_response,
+ assessmentResponsesRows[3].id,
+ ],
+
+ 1
+ );
+
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['Graded'],
+ [{ id: 7 }]
+ );
+
+ mockQuery(
+ 'update `assessment_submissions` set `assessment_submission_state_id` = ?, `score` = ? where `id` = ?',
+ [7, assessmentSubmissions[13].score, assessmentSubmissions[13].id],
+ 1
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[13], true)
+ ).toEqual(assessmentSubmissions[13]);
+ });
+
+ it('should update an existing in-progress assessment submission by a participant with a new SC response', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[1]]
+ );
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ []
+ );
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[0].id],
+ [programAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[0].program_id],
+ [programsRows[0]]
+ );
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ [curriculumAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?',
+ [curriculumAssessmentsRows[0].activity_id],
+ [
+ {
+ title: curriculumAssessments[3].assessment_type,
+ },
+ ]
+ );
+ mockQuery(
+ 'insert into `assessment_responses` (`answer_id`, `assessment_id`, `question_id`, `response`, `submission_id`) values (?, ?, ?, DEFAULT, ?)',
+ [
+ singleChoiceAnswerId,
+ programAssessmentId,
+ singleChoiceQuestionId,
+ assessmentSubmissions[2].id,
+ ],
+ [assessmentSubmissionResponseSCId]
+ );
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['In Progress'],
+ [{ id: 4 }]
+ );
+ mockQuery(
+ 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?',
+ [4, assessmentSubmissions[2].id],
+ 1
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[15], false)
+ ).toEqual(assessmentSubmissions[2]);
+ });
+
+ it('should update an existing in-progress assessment submission by a participant with a new FR response', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[1]]
+ );
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ []
+ );
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[0].id],
+ [programAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[0].program_id],
+ [programsRows[0]]
+ );
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ [curriculumAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?',
+ [curriculumAssessmentsRows[0].activity_id],
+ [
+ {
+ title: curriculumAssessments[3].assessment_type,
+ },
+ ]
+ );
+ mockQuery(
+ 'insert into `assessment_responses` (`answer_id`, `assessment_id`, `question_id`, `response`, `submission_id`) values (DEFAULT, ?, ?, ?, ?)',
+ [
+ programAssessmentId,
+ freeResponseQuestionId,
+ assessmentResponses[7].response_text,
+ assessmentSubmissions[2].id,
+ ],
+ [assessmentSubmissionResponseFRId]
+ );
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['In Progress'],
+ [{ id: 4 }]
+ );
+ mockQuery(
+ 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?',
+ [4, assessmentSubmissions[2].id],
+ 1
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[16], false)
+ ).toEqual(assessmentSubmissions[3]);
+ });
+
+ it('should submit an assessment submission marked as being submitted by updating its state and submission date', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 13, 23, 45);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[1]]
+ );
+
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[1]]
+ );
+
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[0].id],
+ [programAssessmentsRows[0]]
+ );
+
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[0].program_id],
+ [programsRows[0]]
+ );
+
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ [curriculumAssessmentsRows[0]]
+ );
+
+ mockQuery(
+ 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?',
+ [curriculumAssessmentsRows[0].activity_id],
+ [
+ {
+ title: curriculumAssessments[3].assessment_type,
+ },
+ ]
+ );
+
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['Submitted'],
+ [{ id: 6 }]
+ );
+
+ mockQuery(
+ 'update `assessment_submissions` set `assessment_submission_state_id` = ?, `submitted_at` = CURRENT_TIMESTAMP where `id` = ?',
+ [6, assessmentSubmissions[9].id],
+ 1
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[9], false)
+ ).toEqual(assessmentSubmissions[9]);
+ });
+
+ it('should automatically expire an in-progress assessment submission after the due date', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 10, 8, 0, 10);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[1]]
+ );
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[0], assessmentResponsesRows[5]]
+ );
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[1].id],
+ [programAssessmentsRows[1]]
+ );
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[1].program_id],
+ [programsRows[0]]
+ );
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ [curriculumAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?',
+ [curriculumAssessmentsRows[0].activity_id],
+ [
+ {
+ title: curriculumAssessments[3].assessment_type,
+ },
+ ]
+ );
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['Expired'],
+ [{ id: 5 }]
+ );
+ mockQuery(
+ 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?',
+ [5, assessmentSubmissions[2].id],
+ []
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[5], false)
+ ).toEqual(assessmentSubmissions[6]);
+ });
+
+ it('should automatically expire an in-progress assessment submission after the time limit', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 14, 0, 10);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[1]]
+ );
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[0]]
+ );
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[0].id],
+ [programAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[1].program_id],
+ [programsRows[0]]
+ );
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ [curriculumAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?',
+ [curriculumAssessmentsRows[0].activity_id],
+ [
+ {
+ title: curriculumAssessments[3].assessment_type,
+ },
+ ]
+ );
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['Expired'],
+ [{ id: 5 }]
+ );
+ mockQuery(
+ 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?',
+ [5, assessmentSubmissions[2].id],
+ []
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[2], false)
+ ).toEqual(assessmentSubmissions[8]);
+ });
+
+ it('should not allow a participant to modify their responses to an expired submission', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[2]]
+ );
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[1]]
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[2], false)
+ ).toEqual(assessmentSubmissions[7]);
+ });
+
+ it('should not allow a participant to update grading information for themselves', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[3]]
+ );
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[2]]
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[13], false)
+ ).toEqual(assessmentSubmissions[9]);
+ });
+ it('should update an existing in-progress assessment submission by a participant with a changed response for FR response', async () => {
+ const expectedNow = DateTime.utc(2023, 2, 9, 12, 5, 0);
+ Settings.now = () => expectedNow.toMillis();
+
+ mockQuery(
+ 'select `assessment_submissions`.`assessment_id`, `assessment_submissions`.`principal_id`, `assessment_submission_states`.`title` as `assessment_submission_state`, `assessment_submissions`.`score`, `assessment_submissions`.`opened_at`, `assessment_submissions`.`submitted_at`, `assessment_submissions`.`updated_at` from `assessment_submissions` inner join `assessment_submission_states` on `assessment_submissions`.`assessment_submission_state_id` = `assessment_submission_states`.`id` where `assessment_submissions`.`id` = ?',
+ [assessmentSubmissionId],
+ [assessmentSubmissionsRows[1]]
+ );
+ mockQuery(
+ 'select `id`, `assessment_id`, `question_id`, `answer_id`, `response`, `score`, `grader_response` from `assessment_responses` where `submission_id` = ?',
+ [assessmentSubmissionId],
+ [assessmentResponsesRows[4]]
+ );
+ mockQuery(
+ 'select `program_id`, `assessment_id`, `available_after`, `due_date` from `program_assessments` where `id` = ?',
+ [programAssessments[0].id],
+ [programAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `id`, `title`, `start_date`, `end_date`, `time_zone`, `curriculum_id` from `programs` where `id` = ?',
+ [programAssessments[0].program_id],
+ [programsRows[0]]
+ );
+ mockQuery(
+ 'select `curriculum_assessments`.`title`, `curriculum_assessments`.`max_score`, `curriculum_assessments`.`max_num_submissions`, `curriculum_assessments`.`time_limit`, `curriculum_assessments`.`curriculum_id`, `curriculum_assessments`.`activity_id`, `curriculum_assessments`.`principal_id` from `curriculum_assessments` inner join `activities` on `curriculum_assessments`.`curriculum_id` = `activities`.`id` where `curriculum_assessments`.`id` = ?',
+ [curriculumAssessmentId],
+ [curriculumAssessmentsRows[0]]
+ );
+ mockQuery(
+ 'select `activity_types`.`title` from `activity_types` inner join `activities` on `activities`.`activity_type_id` = `activity_types`.`id` where `activities`.`id` = ?',
+ [curriculumAssessmentsRows[0].activity_id],
+ [
+ {
+ title: curriculumAssessments[4].assessment_type,
+ },
+ ]
+ );
+ mockQuery(
+ 'update `assessment_responses` set `response` = ? where `id` = ?',
+ [assessmentResponsesRows[5].response, assessmentResponsesRows[5].id],
+ 1
+ );
+ mockQuery(
+ 'select `id` from `assessment_submission_states` where `title` = ?',
+ ['In Progress'],
+ [{ id: 4 }]
+ );
+ mockQuery(
+ 'update `assessment_submissions` set `assessment_submission_state_id` = ? where `id` = ?',
+ [4, assessmentSubmissions[18].id],
+ 1
+ );
+
+ expect(
+ await updateAssessmentSubmission(assessmentSubmissions[3], false)
+ ).toEqual(assessmentSubmissions[3]);
+ });
+});
+
+describe('updateCurriculumAssessment', () => {
+ it('should update a curriculum assessment with existing questions and updated answers', async () => {
+ mockQuery(
+ 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc',
+ [curriculumAssessments[3].id],
+ [assessmentQuestionsRows[0]]
+ );
+
+ mockQuery(
+ 'select `id`, `question_id`, `title`, `description`, `sort_order` from `assessment_answers` where `question_id` = ? order by `sort_order` asc',
+ [assessmentQuestionsRows[0].id],
+ [assessmentAnswersRows[0]]
+ );
+
+ mockQuery(
+ 'update `assessment_answers` set `title` = ?, `description` = ?, `sort_order` = ? where `id` = ?',
+ [
+ assessmentAnswersRows[2].title,
+ assessmentAnswersRows[2].description,
+ assessmentAnswersRows[2].sort_order,
+ assessmentAnswersRows[2].id,
+ ],
+ [1]
+ );
+
+ mockQuery(
+ 'update `assessment_questions` set `title` = ?, `description` = ?, `correct_answer_id` = ?, `max_score` = ?, `sort_order` = ? where `id` = ?',
+ [
+ assessmentQuestionsRows[0].title,
+ assessmentQuestionsRows[0].description,
+ assessmentQuestionsRows[0].correct_answer_id,
+ assessmentQuestionsRows[0].max_score,
+ assessmentQuestionsRows[0].sort_order,
+ assessmentQuestionsRows[0].id,
+ ],
+ [1]
+ );
+
+ mockQuery(
+ 'update `curriculum_assessments` set `title` = ?, `description` = ?, `max_score` = ?, `max_num_submissions` = ?, `time_limit` = ?, `activity_id` = ? where `id` = ?',
+ [
+ curriculumAssessmentsRows[0].title,
+ curriculumAssessmentsRows[0].description,
+ curriculumAssessmentsRows[0].max_score,
+ curriculumAssessmentsRows[0].max_num_submissions,
+ curriculumAssessmentsRows[0].time_limit,
+ curriculumAssessmentsRows[0].activity_id,
+ curriculumAssessmentsRows[0].id,
+ ],
+ [1]
+ );
+
+ expect(await updateCurriculumAssessment(curriculumAssessments[12])).toEqual(
+ curriculumAssessments[12]
+ );
+ });
+
+ it('should update a curriculum assessment with new questions', async () => {
+ mockQuery(
+ 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc',
+ [curriculumAssessments[3].id],
+ []
+ );
+
+ mockQuery(
+ 'insert into `assessment_questions` (`assessment_id`, `description`, `max_score`, `question_type_id`, `sort_order`, `title`) values (?, ?, ?, ?, ?, ?)',
+ [
+ curriculumAssessmentId,
+ questions[4].description,
+ questions[4].max_score,
+ 1,
+ questions[4].sort_order,
+ questions[4].title,
+ ],
+ [singleChoiceQuestionId]
+ );
+
+ mockQuery(
+ 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)',
+ [
+ answers[4].description,
+ singleChoiceQuestionId,
+ answers[4].sort_order,
+ answers[4].title,
+ ],
+ [singleChoiceAnswerId]
+ );
+
+ mockQuery(
+ 'update `assessment_questions` set `correct_answer_id` = ? where `id` = ?',
+ [assessmentQuestionsRows[0].correct_answer_id, singleChoiceQuestionId],
+ [1]
+ );
+
+ mockQuery(
+ 'update `curriculum_assessments` set `title` = ?, `description` = ?, `max_score` = ?, `max_num_submissions` = ?, `time_limit` = ?, `activity_id` = ? where `id` = ?',
+ [
+ curriculumAssessmentsRows[0].title,
+ curriculumAssessmentsRows[0].description,
+ curriculumAssessmentsRows[0].max_score,
+ curriculumAssessmentsRows[0].max_num_submissions,
+ curriculumAssessmentsRows[0].time_limit,
+ curriculumAssessmentsRows[0].activity_id,
+ curriculumAssessmentsRows[0].id,
+ ],
+ [1]
+ );
+
+ expect(await updateCurriculumAssessment(curriculumAssessments[9])).toEqual(
+ curriculumAssessments[3]
+ );
+ });
+
+ it('should update a curriculum assessment with new question and delete old questions', async () => {
+ mockQuery(
+ 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc',
+ [curriculumAssessments[4].id],
+ [assessmentQuestionsRows[1]]
+ );
+
+ mockQuery(
+ 'delete from `assessment_questions` where `id` = ?',
+ [assessmentQuestionsRows[1].id],
+ [1]
+ );
+
+ mockQuery(
+ 'insert into `assessment_questions` (`assessment_id`, `description`, `max_score`, `question_type_id`, `sort_order`, `title`) values (?, ?, ?, ?, ?, ?)',
+ [
+ curriculumAssessmentId,
+ questions[4].description,
+ questions[4].max_score,
+ 1,
+ questions[4].sort_order,
+ questions[4].title,
+ ],
+ [singleChoiceQuestionId]
+ );
+
+ mockQuery(
+ 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)',
+ [
+ answers[4].description,
+ singleChoiceQuestionId,
+ answers[4].sort_order,
+ answers[4].title,
+ ],
+ [singleChoiceAnswerId]
+ );
+
+ mockQuery(
+ 'update `assessment_questions` set `correct_answer_id` = ? where `id` = ?',
+ [assessmentQuestionsRows[0].correct_answer_id, singleChoiceQuestionId],
+ [1]
+ );
+
+ mockQuery(
+ 'update `curriculum_assessments` set `title` = ?, `description` = ?, `max_score` = ?, `max_num_submissions` = ?, `time_limit` = ?, `activity_id` = ? where `id` = ?',
+ [
+ curriculumAssessmentsRows[0].title,
+ curriculumAssessmentsRows[0].description,
+ curriculumAssessmentsRows[0].max_score,
+ curriculumAssessmentsRows[0].max_num_submissions,
+ curriculumAssessmentsRows[0].time_limit,
+ curriculumAssessmentsRows[0].activity_id,
+ curriculumAssessmentsRows[0].id,
+ ],
+ [1]
+ );
+
+ expect(await updateCurriculumAssessment(curriculumAssessments[10])).toEqual(
+ curriculumAssessments[3]
+ );
+ });
+
+ it('should update a curriculum assessment with new answer and delete existing answer', async () => {
+ mockQuery(
+ 'select `assessment_questions`.`id`, `assessment_questions`.`title`, `description`, `assessment_question_types`.`title` as `question_type`, `correct_answer_id`, `max_score`, `sort_order` from `assessment_questions` inner join `assessment_question_types` on `assessment_questions`.`question_type_id` = `assessment_question_types`.`id` where `assessment_questions`.`assessment_id` = ? order by `sort_order` asc',
+ [curriculumAssessments[3].id],
+ [assessmentQuestionsRows[0]]
+ );
+
+ mockQuery(
+ 'select `id`, `question_id`, `title`, `description`, `sort_order` from `assessment_answers` where `question_id` = ? order by `sort_order` asc',
+ [assessmentQuestionsRows[0].id],
+ [assessmentAnswersRows[0]]
+ );
+
+ mockQuery(
+ 'delete from `assessment_answers` where `id` = ?',
+ [assessmentAnswersRows[0].id],
+ [1]
+ );
+
+ mockQuery(
+ 'insert into `assessment_answers` (`description`, `question_id`, `sort_order`, `title`) values (?, ?, ?, ?)',
+ [
+ answers[4].description,
+ singleChoiceQuestionId,
+ answers[4].sort_order,
+ answers[4].title,
+ ],
+ [singleChoiceAnswerId]
+ );
+
+ mockQuery(
+ 'update `assessment_questions` set `title` = ?, `description` = ?, `correct_answer_id` = ?, `max_score` = ?, `sort_order` = ? where `id` = ?',
+ [
+ assessmentQuestionsRows[0].title,
+ assessmentQuestionsRows[0].description,
+ singleChoiceAnswerId,
+ assessmentQuestionsRows[0].max_score,
+ assessmentQuestionsRows[0].sort_order,
+ assessmentQuestionsRows[0].id,
+ ],
+ [1]
+ );
+
+ mockQuery(
+ 'update `curriculum_assessments` set `title` = ?, `description` = ?, `max_score` = ?, `max_num_submissions` = ?, `time_limit` = ?, `activity_id` = ? where `id` = ?',
+ [
+ curriculumAssessmentsRows[0].title,
+ curriculumAssessmentsRows[0].description,
+ curriculumAssessmentsRows[0].max_score,
+ curriculumAssessmentsRows[0].max_num_submissions,
+ curriculumAssessmentsRows[0].time_limit,
+ curriculumAssessmentsRows[0].activity_id,
+ curriculumAssessmentsRows[0].id,
+ ],
+ [1]
+ );
+
+ expect(await updateCurriculumAssessment(curriculumAssessments[11])).toEqual(
+ curriculumAssessments[3]
+ );
+ });
+});
+
+describe('updateProgramAssessment', () => {
+ it('should return update for an existing program assessment ID', async () => {
+ mockQuery(
+ 'update `program_assessments` set `available_after` = ?, `due_date` = ? where `id` = ?',
+ [
+ programAssessmentsRows[3].available_after,
+ programAssessmentsRows[3].due_date,
+ programAssessmentsRows[3].id,
+ ],
+ []
+ );
+
+ expect(await updateProgramAssessment(programAssessmentsRows[3])).toEqual(
+ programAssessmentsRows[3]
+ );
+ });
+});
diff --git a/api/src/services/__tests__/questionnairesService.ts b/api/src/services/__tests__/questionnairesService.ts
index 5879d4f5..9305e72f 100644
--- a/api/src/services/__tests__/questionnairesService.ts
+++ b/api/src/services/__tests__/questionnairesService.ts
@@ -14,17 +14,17 @@ describe('questionnairesService', () => {
};
const options = [{ id: 3, label: 'option label', prompt_id: promptId }];
mockQuery(
- 'select `id` from `questionnaires` where `id` = ?',
+ 'select `id` from `surveys` where `id` = ?',
[questionnaireId],
[questionnaire]
);
mockQuery(
- 'select `id`, `label`, `query_text` from `prompts` where `questionnaire_id` = ? order by `sort_order` asc',
+ 'select `id`, `label`, `query_text` from `survey_questions` where `questionnaire_id` = ? order by `sort_order` asc',
[questionnaireId],
[prompt]
);
mockQuery(
- 'select `id`, `label`, `prompt_id` from `options` where `prompt_id` in (?) order by `prompt_id` asc',
+ 'select `id`, `label`, `prompt_id` from `survey_answers` where `prompt_id` in (?) order by `prompt_id` asc',
[promptId],
options
);
@@ -37,7 +37,7 @@ describe('questionnairesService', () => {
it('should return null if no questionnaire was found with the given id', async () => {
const questionnaireId = 1;
mockQuery(
- 'select `id` from `questionnaires` where `id` = ?',
+ 'select `id` from `surveys` where `id` = ?',
[questionnaireId],
[]
);
diff --git a/api/src/services/__tests__/reflectionsService.ts b/api/src/services/__tests__/reflectionsService.ts
index 8a10bdc4..321e77aa 100644
--- a/api/src/services/__tests__/reflectionsService.ts
+++ b/api/src/services/__tests__/reflectionsService.ts
@@ -58,7 +58,7 @@ describe('reflectionsService', () => {
journalEntries
);
mockQuery(
- 'select `responses`.`id` as `id`, `reflection_id` as `reflection_id`, `option_id` as `option_id`, `options`.`label` as `option_label`, `prompt_id` as `prompt_id`, `prompts`.`label` as `prompt_label` from `responses` inner join `options` on `responses`.`option_id` = `options`.`id` inner join `prompts` on `options`.`prompt_id` = `prompts`.`id` where `reflection_id` in (?) order by `prompts`.`sort_order` asc',
+ 'select `responses`.`id` as `id`, `reflection_id` as `reflection_id`, `option_id` as `option_id`, `survey_answers`.`label` as `option_label`, `prompt_id` as `prompt_id`, `survey_questions`.`label` as `prompt_label` from `responses` inner join `survey_answers` on `responses`.`option_id` = `survey_answers`.`id` inner join `survey_questions` on `survey_answers`.`prompt_id` = `survey_questions`.`id` where `reflection_id` in (?) order by `survey_questions`.`sort_order` asc',
[reflectionId],
responses
);
diff --git a/api/src/services/assessmentsService.ts b/api/src/services/assessmentsService.ts
new file mode 100644
index 00000000..20e226b9
--- /dev/null
+++ b/api/src/services/assessmentsService.ts
@@ -0,0 +1,1769 @@
+import { DateTime } from 'luxon';
+import {
+ Answer,
+ AssessmentResponse,
+ AssessmentSubmission,
+ CurriculumAssessment,
+ FacilitatorAssessmentSubmissionsSummary,
+ ParticipantAssessmentSubmissionsSummary,
+ ProgramAssessment,
+ Question,
+} from '../models';
+import db from './db';
+import { findProgram, listProgramsForCurriculum } from './programsService';
+
+/**
+ * Determines whether or not a specific program assessment submission has
+ * expired and is no longer allowed to be updated by the participant.
+ *
+ * @param {AssessmentSubmission} assessmentSubmission - The assessment
+ * submission to check for expiration, passed as an AssessmentSubmission
+ * object.
+ * @returns {Promise} If true, a participant should be prevented from
+ * submitting any updates to their program assessment submission, other than
+ * to mark it as "Expired" instead of "Opened" or "In Progress".
+ */
+const assessmentSubmissionExpired = async (
+ assessmentSubmission: AssessmentSubmission
+): Promise => {
+ const programAssessment = await findProgramAssessment(
+ assessmentSubmission.assessment_id
+ );
+ const curriculumAssessment = await getCurriculumAssessment(
+ programAssessment.assessment_id,
+ false,
+ false
+ );
+
+ if (DateTime.now() > DateTime.fromISO(programAssessment.due_date)) {
+ return true;
+ }
+
+ if (
+ curriculumAssessment.time_limit &&
+ typeof curriculumAssessment.time_limit === 'number' &&
+ curriculumAssessment.time_limit > 0
+ ) {
+ const endTime = DateTime.fromISO(assessmentSubmission.opened_at).plus({
+ minutes: curriculumAssessment.time_limit,
+ });
+ if (DateTime.now() >= endTime) {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+/**
+ * Calculates the total number of participants in a program that have started or
+ * completed *any* submission for a given program assessment.
+ *
+ * @param {number} programAssessmentId - The row ID of the program_assessments
+ * table for a given program assessment.
+ * @returns {Promise} The number of program participants with one or
+ * more submissions for that program assessment.
+ */
+const calculateNumParticipantsWithSubmissions = async (
+ programAssessmentId: number
+): Promise => {
+ const [numParticipantsWithSubmissions] = await db('assessment_submissions')
+ .where('assessment_id', programAssessmentId)
+ .countDistinct({ count: 'principal_id' });
+
+ return numParticipantsWithSubmissions.count;
+};
+
+/**
+ * Calculates the total number of participants enrolled in a program, excluding
+ * any program facilitators.
+ *
+ * @param {number} programId - The row ID of the programs table for a given
+ * program.
+ * @returns {Promise} The number of program participants in that
+ * program.
+ */
+const calculateNumProgramParticipants = async (
+ programId: number
+): Promise => {
+ const [matchingProgramParticipantsResults] = await db('program_participants')
+ .where('program_id', programId)
+ .andWhere('role_id', 2)
+ .count({ count: 'id' });
+
+ return matchingProgramParticipantsResults.count;
+};
+
+/**
+ * Calculates the total number of assessment submissions that have yet to be
+ * graded for a given program assessment.
+ *
+ * @param {number} programAssessmentId - The row ID of the program_assessments
+ * table for a given program assessment.
+ * @returns {Promise} The number of assessment submissions that have
+ * been submitted but not graded.
+ */
+const calculateNumUngradedSubmissions = async (
+ programAssessmentId: number
+): Promise => {
+ const [numUngradedSubmissions] = await db('assessment_submissions')
+ .where('assessment_id', programAssessmentId)
+ .andWhere('score', null)
+ .count({ count: 'id' });
+
+ return numUngradedSubmissions.count;
+};
+
+/**
+ * Inserts a question for an existing curriculum assessment into the
+ * assessment_questions table.
+ *
+ * @param {number} curriculumAssessmentId - The curriculum assessment to which
+ * we are adding this question.
+ * @param {Question} question - An object containing the question, its metadata,
+ * and any possible answers.
+ * @returns {Promise} The updated Question object that was handed to
+ * us but with updated row IDs for the question and all answers given to us.
+ */
+const createAssessmentQuestion = async (
+ curriculumAssessmentId: number,
+ question: Question
+): Promise => {
+ const [insertedAssessmentQuestionId] = await db(
+ 'assessment_questions'
+ ).insert({
+ assessment_id: curriculumAssessmentId,
+ title: question.title,
+ description: question.description,
+ question_type_id: question.question_type === 'single choice' ? 1 : 2,
+ max_score: question.max_score,
+ sort_order: question.sort_order,
+ });
+
+ const insertedAnswers: Answer[] = [];
+ let correctAnswerId;
+
+ for (const assessmentAnswer of question.answers) {
+ const insertedAnswer = await createAssessmentQuestionAnswer(
+ insertedAssessmentQuestionId,
+ assessmentAnswer
+ );
+
+ if (question.question_type === 'single choice') {
+ if (insertedAnswer.correct_answer === true) {
+ correctAnswerId = insertedAnswer.id;
+ }
+ } else if (question.question_type === 'free response') {
+ correctAnswerId = insertedAnswer.id;
+ }
+ insertedAnswers.push(insertedAnswer);
+ }
+
+ const updatedAssessmentQuestion = {
+ ...question,
+ id: insertedAssessmentQuestionId,
+ answers: insertedAnswers,
+ };
+
+ if (correctAnswerId) {
+ await db('assessment_questions')
+ .update('correct_answer_id', correctAnswerId)
+ .where('id', insertedAssessmentQuestionId);
+ updatedAssessmentQuestion.correct_answer_id = correctAnswerId;
+ }
+
+ return updatedAssessmentQuestion;
+};
+
+/**
+ * Inserts an answer for an existing curriculum assessment question into the
+ * assessment_answers table.
+ *
+ * @param {number} questionId - The row ID of the assessment_questions table for
+ * a given question.
+ * @param {Answer} answer - An object containing an answer option and its
+ * metadata.
+ * @returns {Promise} The updated Answer object that was handed to us
+ * but with row ID specified.
+ */
+const createAssessmentQuestionAnswer = async (
+ questionId: number,
+ answer: Answer
+): Promise => {
+ const [insertedAssessmentAnswersId] = await db('assessment_answers').insert({
+ question_id: questionId,
+ title: answer.title,
+ description: answer.description,
+ sort_order: answer.sort_order,
+ });
+
+ const updatedAssessmentAnswer = {
+ ...answer,
+ question_id: questionId,
+ id: insertedAssessmentAnswersId,
+ };
+
+ return updatedAssessmentAnswer;
+};
+
+/**
+ * Inserts a response for a user for a given curriculum assessment question into
+ * the assessment_responses table. It does not check if the question relates to
+ * the curriculum assessment for which the submission relates, nor does it check
+ * to see if the answer relates to the question (for single choice questions).
+ *
+ * @param {AssessmentResponse} assessmentResponse - An object containing the
+ * assessment response data.
+ * @returns {Promise} The updated AssessmentResponse object
+ * that was handed to us but with row ID specified.
+ */
+const createSubmissionResponse = async (
+ assessmentResponse: AssessmentResponse
+): Promise => {
+ const [newSubmissionResponseId] = await db('assessment_responses').insert({
+ assessment_id: assessmentResponse.assessment_id,
+ submission_id: assessmentResponse.submission_id,
+ question_id: assessmentResponse.question_id,
+ answer_id: assessmentResponse.answer_id,
+ response: assessmentResponse.response_text,
+ });
+
+ const newSubmissionResponse: AssessmentResponse = {
+ id: newSubmissionResponseId,
+ assessment_id: assessmentResponse.assessment_id,
+ submission_id: assessmentResponse.submission_id,
+ question_id: assessmentResponse.question_id,
+ };
+
+ if (assessmentResponse.answer_id) {
+ newSubmissionResponse.answer_id = assessmentResponse.answer_id;
+ } else if (assessmentResponse.response_text) {
+ newSubmissionResponse.response_text = assessmentResponse.response_text;
+ }
+
+ return newSubmissionResponse;
+};
+
+/**
+ * Removes an existing question from a curriculum assessment without deleting
+ * the entire assessment.
+ *
+ * @param {number} questionId - The row ID of the assessment_questions table for
+ * a given question.
+ * @returns {Promise} Returns nothing if the deletion was successful.
+ */
+const deleteAssessmentQuestion = async (questionId: number): Promise => {
+ return db('assessment_questions').where('id', questionId).delete();
+};
+
+/**
+ * Removes an existing answer option from a curriculum assessment question
+ * without deleting the question.
+ *
+ * @param {number} answerId - The row ID of the assessment_answers table for a
+ * given answer.
+ * @returns {Promise} Returns nothing if the deletion was successful.
+ */
+const deleteAssessmentQuestionAnswer = async (
+ answerId: number
+): Promise => {
+ return db('assessment_answers').where('id', answerId).delete();
+};
+
+/**
+ * Lists all possible answer options for a given curriculum assessment question.
+ * If specified, will also return metadata indicating which answer option is
+ * correct.
+ *
+ * @param {number} questionId - The row ID of the assessment_questions table for
+ * a given question.
+ * @returns {Promise} An array of Answer options, including or
+ * omitting the correct answer metadata as specified.
+ */
+const listAssessmentQuestionAnswers = async (
+ questionId: number
+): Promise => {
+ const assessmentAnswersList = await db('assessment_answers')
+ .select('id', 'question_id', 'title', 'description', 'sort_order')
+ .where('question_id', questionId)
+ .orderBy('sort_order', 'asc');
+
+ return assessmentAnswersList;
+};
+
+/**
+ * Lists all questions of a given curriculum assessment. Based on specified
+ * boolean parameter, will also return metadata indicating the correct answer
+ * option. Note that for free response questions, this function will not return
+ * any answers unless correctAnswersIncluded is set to true.
+ *
+ * @param {number} curriculumAssessmentId - The row ID of the
+ * curriculum_assessments table for a given curriculum assessment.
+ * @param {boolean} [answersIncluded] - Optional specifier to determine whether
+ * or not the answers for the questions should be included or removed from the
+ * return value.
+ * @param {boolean} [correctAnswersIncluded] - Optional specifier to determine
+ * whether or not the correct answer information should be included or removed
+ * from the return value.
+ * @returns {Promise} An array of Question objects, including or
+ * omitting the correct answer metadata as specified.
+ */
+const listAssessmentQuestions = async (
+ curriculumAssessmentId: number,
+ answersIncluded?: boolean,
+ correctAnswersIncluded?: boolean
+): Promise => {
+ const matchingAssessmentQuestionsRows = await db('assessment_questions')
+ .join(
+ 'assessment_question_types',
+ 'assessment_questions.question_type_id',
+ 'assessment_question_types.id'
+ )
+ .select(
+ 'assessment_questions.id',
+ 'assessment_questions.title',
+ 'description',
+ 'assessment_question_types.title as question_type',
+ 'correct_answer_id',
+ 'max_score',
+ 'sort_order'
+ )
+ .where('assessment_questions.assessment_id', curriculumAssessmentId)
+ .orderBy('sort_order', 'asc');
+
+ if (matchingAssessmentQuestionsRows.length === 0) {
+ return null;
+ }
+
+ const assessmentQuestions: Question[] = [];
+
+ for (const assessmentQuestionsRow of matchingAssessmentQuestionsRows) {
+ const assessmentQuestion: Question = {
+ id: assessmentQuestionsRow.id,
+ assessment_id: curriculumAssessmentId,
+ title: assessmentQuestionsRow.title,
+ description: assessmentQuestionsRow.description,
+ question_type: assessmentQuestionsRow.question_type,
+ max_score: assessmentQuestionsRow.max_score,
+ sort_order: assessmentQuestionsRow.sort_order,
+ };
+
+ if (answersIncluded && answersIncluded === true) {
+ assessmentQuestion.answers = await listAssessmentQuestionAnswers(
+ assessmentQuestionsRow.id
+ );
+
+ if (correctAnswersIncluded && correctAnswersIncluded === true) {
+ assessmentQuestion.correct_answer_id =
+ assessmentQuestionsRow.correct_answer_id;
+ const correctAnswer = assessmentQuestion.answers.find(
+ answer => answer.id === assessmentQuestionsRow.correct_answer_id
+ );
+ if (correctAnswer) {
+ correctAnswer.correct_answer = true;
+ }
+ }
+ }
+
+ assessmentQuestions.push(assessmentQuestion);
+ }
+
+ return assessmentQuestions;
+};
+
+/**
+ * Lists all responses from a given assessment submission by a program
+ * participant. Based on specified boolean parameter, will also include the
+ * score and any grader response as well.
+ *
+ * @param {number} submissionId - The row ID of the assessment_submissions table
+ * for a given program assessment submission.
+ * @param {boolean} [gradingsIncluded] - Optional specifier to determine whether
+ * or not the grading information (score, grader response) should be included
+ * or removed from the return value.
+ * @returns {Promise} An array of AssessmentResponse
+ * objects, including or omitting the grading information as specified.
+ */
+const listSubmissionResponses = async (
+ submissionId: number,
+ gradingsIncluded?: boolean
+): Promise => {
+ const matchingAssessmentResponsesRows = await db('assessment_responses')
+ .select(
+ 'id',
+ 'assessment_id',
+ 'question_id',
+ 'answer_id',
+ 'response',
+ 'score',
+ 'grader_response'
+ )
+ .where('submission_id', submissionId);
+
+ if (matchingAssessmentResponsesRows.length === 0) {
+ return null;
+ }
+
+ const assessmentResponses: AssessmentResponse[] =
+ matchingAssessmentResponsesRows.map(assessmentResponsesRow => {
+ const assessmentResponse: AssessmentResponse = {
+ id: assessmentResponsesRow.id,
+ assessment_id: assessmentResponsesRow.assessment_id,
+ submission_id: submissionId,
+ question_id: assessmentResponsesRow.question_id,
+ };
+
+ if (
+ assessmentResponsesRow.answer_id === null &&
+ assessmentResponsesRow.response !== null
+ ) {
+ assessmentResponse.response_text = assessmentResponsesRow.response;
+ } else if (assessmentResponsesRow.answer_id !== null) {
+ assessmentResponse.answer_id = assessmentResponsesRow.answer_id;
+ }
+
+ if (gradingsIncluded && gradingsIncluded === true) {
+ assessmentResponse.score = assessmentResponsesRow.score;
+ assessmentResponse.grader_response =
+ assessmentResponsesRow.grader_response;
+ }
+
+ return assessmentResponse;
+ });
+
+ return assessmentResponses;
+};
+
+/**
+ * Updates an existing curriculum assessment question with new answer options or
+ * new metadata.
+ *
+ * @param {Question} question - The Question object for a given curriculum
+ * assessment question with updated information.
+ * @returns {Promise} The updated Question object, including created
+ * or updated Answer objects, if any.
+ */
+const updateAssessmentQuestion = async (
+ question: Question
+): Promise => {
+ const existingAnswers = await listAssessmentQuestionAnswers(question.id);
+
+ const updatedAnswers: Answer[] = [];
+ const newAnswers: Answer[] = [];
+
+ const updatedQuestion = {
+ ...question,
+ };
+
+ let correctAnswerId = question.correct_answer_id;
+
+ if (
+ question.answers &&
+ Array.isArray(question.answers) &&
+ question.answers.length > 0
+ ) {
+ for (const answer of question.answers) {
+ if (answer.id && typeof answer.id === 'number' && answer.id > 0) {
+ if (
+ existingAnswers &&
+ Array.isArray(existingAnswers) &&
+ existingAnswers.length > 0
+ ) {
+ const eqIndex = existingAnswers.findIndex(
+ existingAnswer => existingAnswer.id === answer.id
+ );
+ existingAnswers.splice(eqIndex, 1);
+ }
+ updatedAnswers.push(answer);
+
+ if (answer.correct_answer && answer.correct_answer === true) {
+ correctAnswerId = answer.id;
+ }
+ } else {
+ newAnswers.push(answer);
+ }
+ }
+ }
+
+ if (
+ existingAnswers &&
+ Array.isArray(existingAnswers) &&
+ existingAnswers.length > 0
+ ) {
+ for (const deletedAnswer of existingAnswers) {
+ await deleteAssessmentQuestionAnswer(deletedAnswer.id);
+ }
+ }
+
+ const newAnswersList: Answer[] = [];
+
+ for (const newAnswer of newAnswers) {
+ const newAnswerInserted = await createAssessmentQuestionAnswer(
+ question.id,
+ newAnswer
+ );
+
+ if (newAnswer.correct_answer && newAnswer.correct_answer === true) {
+ correctAnswerId = newAnswerInserted.id;
+ }
+
+ newAnswersList.push(newAnswerInserted);
+ }
+
+ for (const updatedAnswer of updatedAnswers) {
+ newAnswersList.push(await updateAssessmentQuestionAnswer(updatedAnswer));
+ }
+
+ updatedQuestion.correct_answer_id = correctAnswerId;
+
+ await db('assessment_questions')
+ .update({
+ title: question.title,
+ description: question.description,
+ correct_answer_id: correctAnswerId,
+ max_score: question.max_score,
+ sort_order: question.sort_order,
+ })
+ .where('id', question.id);
+
+ updatedQuestion.answers = newAnswersList;
+
+ return updatedQuestion;
+};
+
+/**
+ * Updates an existing answer option for a curriculum assessment question with
+ * new metadata.
+ *
+ * @param {Answer} answer - The Answer object for a given curriculum assessment
+ * question answer option with updated information.
+ * @returns {Promise} The updated Answer object.
+ */
+const updateAssessmentQuestionAnswer = async (
+ answer: Answer
+): Promise => {
+ await db('assessment_answers')
+ .update({
+ title: answer.title,
+ description: answer.description,
+ sort_order: answer.sort_order,
+ })
+ .where('id', answer.id);
+ return answer;
+};
+
+/**
+ * Updates an existing assessment submission response with updated metadata. It
+ * does not check if the question relates to the curriculum assessment for which
+ * the submission relates, nor does it check to see if the answer relates to the
+ * question (for single choice questions).
+ *
+ * @param {AssessmentResponse} assessmentResponse - The AssessmentResponse
+ * object for a given program assessment submission response with updated
+ * information.
+ * @param {boolean} [facilitatorGrading] - Optional specifier for when the
+ * program facilitator is the one updating a program assessment submission
+ * response, so that a facilitator is allowed to modify the score and grader
+ * response instead of a participant who is allowed to update their answer.
+ * @returns {Promise} The updated AssessmentResponse object.
+ */
+const updateSubmissionResponse = async (
+ assessmentResponse: AssessmentResponse,
+ facilitatorGrading?: boolean
+): Promise => {
+ if (facilitatorGrading && facilitatorGrading === true) {
+ await db('assessment_responses')
+ .update({
+ score: assessmentResponse.score,
+ grader_response: assessmentResponse.grader_response,
+ })
+ .where('id', assessmentResponse.id);
+ } else {
+ await db('assessment_responses')
+ .update({
+ answer_id: assessmentResponse.answer_id,
+ response: assessmentResponse.response_text,
+ })
+ .where('id', assessmentResponse.id);
+ }
+
+ return assessmentResponse;
+};
+
+/**
+ * Gathers the relevant information for constructing a
+ * FacilitatorAssessmentSubmissionsSummary for a given program assessment.
+ *
+ * @param {number} programAssessment - The program assessment for which we're
+ * constructing a summary.
+ * @returns {Promise} The program
+ * assessment submissions summary information for use by a program
+ * facilitator.
+ */
+export const constructFacilitatorAssessmentSummary = async (
+ programAssessment: ProgramAssessment
+): Promise => {
+ const numParticipantsWithSubmissions =
+ await calculateNumParticipantsWithSubmissions(programAssessment.id);
+ const numProgramParticipants = await calculateNumProgramParticipants(
+ programAssessment.program_id
+ );
+ const numUngradedSubmissions = await calculateNumUngradedSubmissions(
+ programAssessment.id
+ );
+
+ const facilitatorAssessmentSummary: FacilitatorAssessmentSubmissionsSummary =
+ {
+ num_participants_with_submissions: numParticipantsWithSubmissions,
+ num_program_participants: numProgramParticipants,
+ num_ungraded_submissions: numUngradedSubmissions,
+ };
+
+ return facilitatorAssessmentSummary;
+};
+
+/**
+ * Gathers the relevant information for constructing a
+ * ParticipantAssessmentSubmissionsSummary for a given participant principal ID
+ * and a given program assessment.
+ *
+ * @param {number} participantPrincipalId - The row ID of the principals table
+ * that corresponds with a given program participant.
+ * @param {ProgramAssessment} programAssessment - The program assessment for
+ * which we're constructing a summary.
+ * @returns {Promise} The program
+ * assessment submissions summary information for use by a program
+ * participant.
+ */
+export const constructParticipantAssessmentSummary = async (
+ participantPrincipalId: number,
+ programAssessment: ProgramAssessment
+): Promise => {
+ const assessmentActiveDate = DateTime.fromISO(
+ programAssessment.available_after
+ );
+ const assessmentDueDate = DateTime.fromISO(programAssessment.due_date);
+
+ let highestState;
+
+ const highestStateFromDB = await db('assessment_submissions')
+ .select('assessment_submission_states.title')
+ .join(
+ 'assessment_submission_states',
+ 'assessment_submission_states.id',
+ 'assessment_submissions.assessment_submission_state_id'
+ )
+ .where('assessment_submissions.principal_id', participantPrincipalId)
+ .andWhere('assessment_submissions.assessment_id', programAssessment.id)
+ .orderBy('assessment_submissions.assessment_submission_state_id', 'desc')
+ .limit(1);
+
+ if (highestStateFromDB.length === 0) {
+ if (
+ DateTime.now() >= assessmentActiveDate &&
+ DateTime.now() < assessmentDueDate
+ ) {
+ highestState = 'Active';
+ } else if (DateTime.now() < assessmentActiveDate) {
+ highestState = 'Inactive';
+ } else if (DateTime.now() >= assessmentDueDate) {
+ highestState = 'Expired';
+ }
+ } else {
+ highestState = highestStateFromDB[0].title;
+ }
+
+ let mostRecentSubmittedDate;
+ const mostRecentSubmittedDateFromDB = await db('assessment_submissions')
+ .select('submitted_at')
+ .where('principal_id', participantPrincipalId)
+ .andWhere('assessment_id', programAssessment.id)
+ .orderBy('submitted_at', 'desc')
+ .limit(1);
+
+ if (mostRecentSubmittedDateFromDB.length === 0) {
+ mostRecentSubmittedDate = null;
+ } else {
+ mostRecentSubmittedDate = DateTime.fromSQL(
+ mostRecentSubmittedDateFromDB[0].submitted_at,
+ { zone: 'utc' }
+ ).toISO();
+ }
+
+ const totalNumSubmissions = await listParticipantProgramAssessmentSubmissions(
+ participantPrincipalId,
+ programAssessment.id
+ );
+
+ let highestScore;
+ const highestScoreFromDB = await db('assessment_submissions')
+ .select('score')
+ .where('principal_id', participantPrincipalId)
+ .andWhere('assessment_id', programAssessment.id)
+ .orderBy('score', 'desc')
+ .limit(1);
+
+ if (highestScoreFromDB.length === 0) {
+ highestScore = null;
+ } else {
+ highestScore = highestScoreFromDB[0].score;
+ }
+
+ const participantAssessmentSummary: ParticipantAssessmentSubmissionsSummary =
+ {
+ principal_id: participantPrincipalId,
+ highest_state: highestState,
+ total_num_submissions: totalNumSubmissions
+ ? totalNumSubmissions.length
+ : 0,
+ };
+
+ if (mostRecentSubmittedDate !== null) {
+ participantAssessmentSummary.most_recent_submitted_date =
+ mostRecentSubmittedDate;
+ }
+
+ if (highestScore !== null) {
+ participantAssessmentSummary.highest_score = highestScore;
+ }
+
+ return participantAssessmentSummary;
+};
+
+/**
+ * Begins a new program assessment submission for a program participant, if they
+ * have not exceeded the maximum number of allowed submissions for that
+ * assessment and no other assessment submissions are in progress by that
+ * program participant for that program assessment.
+ *
+ * @param {number} participantPrincipalId - The row ID of the principals table
+ * that corresponds with a given program participant.
+ * @param {number} programAssessmentId - The row ID of the program_assessments
+ * table for a given program assessment.
+ * @param {number} curriculumAssessmentId - The row ID of the
+ * curriculum_assessments table, to save us a database query.
+ * @returns {Promise} An AssessmentSubmission object
+ * constructed from the inserted row in the assessment_submissions table.
+ */
+export const createAssessmentSubmission = async (
+ participantPrincipalId: number,
+ programAssessmentId: number,
+ curriculumAssessmentId: number
+): Promise => {
+ const openedStateTitle = 'Opened';
+ const [openedStateId] = await db('assessment_submission_states')
+ .select('id')
+ .where('title', openedStateTitle);
+
+ const [newSubmissionId] = await db('assessment_submissions').insert({
+ assessment_id: programAssessmentId,
+ principal_id: participantPrincipalId,
+ assessment_submission_state_id: openedStateId.id,
+ });
+
+ const assessmentQuestionIds = await db('assessment_questions')
+ .select('id')
+ .where({ assessment_id: curriculumAssessmentId });
+
+ const submissionResponses: AssessmentResponse[] = [];
+
+ for (const question of assessmentQuestionIds) {
+ const response = await createSubmissionResponse({
+ assessment_id: programAssessmentId,
+ submission_id: newSubmissionId,
+ question_id: question.id,
+ });
+
+ submissionResponses.push(response);
+ }
+
+ const newSubmission: AssessmentSubmission = {
+ id: newSubmissionId,
+ assessment_id: programAssessmentId,
+ principal_id: participantPrincipalId,
+ assessment_submission_state: openedStateTitle,
+ opened_at: DateTime.now().toUTC().toISO(),
+ last_modified: DateTime.now().toUTC().toISO(),
+ responses: submissionResponses,
+ };
+
+ return newSubmission;
+};
+
+/**
+ * Creates a new curriculum assessment in the curriculum_assessments table,
+ * linked with a given curriculum activity.
+ *
+ * @param {CurriculumAssessment} curriculumAssessment - The CurriculumAssessment
+ * object for the new curriculum assessment data to be inserted.
+ * @returns {Promise} The updated CurriculumAssessment
+ * object that was handed to us, but with row ID specified.
+ */
+export const createCurriculumAssessment = async (
+ curriculumAssessment: CurriculumAssessment
+): Promise => {
+ const [insertedCurriculumAssessmentRowId] = await db(
+ 'curriculum_assessments'
+ ).insert({
+ title: curriculumAssessment.title,
+ description: curriculumAssessment.description,
+ max_score: curriculumAssessment.max_score,
+ max_num_submissions: curriculumAssessment.max_num_submissions,
+ time_limit: curriculumAssessment.time_limit,
+ curriculum_id: curriculumAssessment.curriculum_id,
+ activity_id: curriculumAssessment.activity_id,
+ principal_id: curriculumAssessment.principal_id,
+ });
+
+ const insertedQuestions: Question[] = [];
+
+ if (typeof curriculumAssessment.questions !== 'undefined') {
+ for (const assessmentQuestion of curriculumAssessment.questions) {
+ insertedQuestions.push(
+ await createAssessmentQuestion(
+ insertedCurriculumAssessmentRowId,
+ assessmentQuestion
+ )
+ );
+ }
+ }
+
+ const updatedCurriculumAssessment: CurriculumAssessment = {
+ ...curriculumAssessment,
+ id: insertedCurriculumAssessmentRowId,
+ };
+
+ if (insertedQuestions.length > 0) {
+ updatedCurriculumAssessment.questions = insertedQuestions;
+ }
+
+ return updatedCurriculumAssessment;
+};
+
+/**
+ * Creates a new program assessment in the program_assessments table, linked
+ * with a given curriculum assessment.
+ *
+ * @param {ProgramAssessment} programAssessment - The ProgramAssessment object
+ * for the new program assessment data to be inserted.
+ * @returns {Promise} The updated ProgramAssessment object
+ * that was handed to us, but with row ID specified.
+ */
+export const createProgramAssessment = async (
+ programAssessment: ProgramAssessment
+): Promise => {
+ const [insertedProgramAssessmentRowId] = await db(
+ 'program_assessments'
+ ).insert({
+ program_id: programAssessment.program_id,
+ assessment_id: programAssessment.assessment_id,
+ available_after: programAssessment.available_after,
+ due_date: programAssessment.due_date,
+ });
+
+ const program = await findProgram(programAssessment.program_id);
+
+ const updatedProgramAssessment: ProgramAssessment = {
+ ...programAssessment,
+ id: insertedProgramAssessmentRowId,
+ available_after: DateTime.fromSQL(programAssessment.available_after, {
+ zone: program.time_zone,
+ }).toISO(),
+ due_date: DateTime.fromSQL(programAssessment.due_date, {
+ zone: program.time_zone,
+ }).toISO(),
+ };
+
+ return updatedProgramAssessment;
+};
+
+/**
+ * Deletes a given curriculum assessment, all associated program assessments,
+ * and all associated questions and answers for a given curriculum assessment.
+ * This function fails to execute if there has ever been an assessment
+ * submission for the questions and answers in this curriculum assessment.
+ *
+ * @param {number} curriculumAssessmentId - The row ID of the
+ * curriculum_assessments table for a given curriculum assessment.
+ * @returns {Promise} Returns nothing if the deletion was successful.
+ */
+export const deleteCurriculumAssessment = async (
+ curriculumAssessmentId: number
+): Promise => {
+ return db('curriculum_assessments')
+ .where('id', curriculumAssessmentId)
+ .delete();
+};
+
+/**
+ * Deletes a given program assessment, but leaves the curriculum assessment, its
+ * questions, and its answers intact. This function fails to execute if there
+ * has ever been an assessment submission for this program assessment by a
+ * program participant.
+ *
+ * @param {number} programAssessmentId - The row ID of the program_assessments
+ * table for a given program assessment.
+ * @returns {Promise} Returns nothing if the deletion was successful.
+ */
+export const deleteProgramAssessment = async (
+ programAssessmentId: number
+): Promise => {
+ return db('program_assessments').where('id', programAssessmentId).delete();
+};
+
+/**
+ * For demonstration purposes only and should be removed from the shipping
+ * product. Enrolls a facilitator in a program so that the assessments list
+ * feature can be demonstrated.
+ *
+ * @param {number} principalId - The row ID of the principals table
+ * corresponding with the user to enroll as a facilitator in a program.
+ * @param {number} programId - The row ID of the programs table corresponding
+ * with the program into which the facilitator should be enrolled.
+ * @returns {Promise} Returns `true` if the user was successfully
+ * enrolled in the program as a facilitator or if the user was switched to the
+ * facilitator role; returns `false` if the user was already a facilitator in
+ * the program.
+ */
+export const enrollFacilitator = async (
+ principalId: number,
+ programId: number
+): Promise => {
+ const isEnrolled = await db('program_participants')
+ .where({
+ principal_id: principalId,
+ program_id: programId,
+ })
+ .select('role_id');
+
+ if (isEnrolled.length > 0) {
+ if (isEnrolled[0].role_id !== 1) {
+ await db('program_participants')
+ .where({ principal_id: principalId, program_id: programId })
+ .update({ role_id: 1 });
+ return true;
+ }
+ return false;
+ } else {
+ await db('program_participants').insert({
+ principal_id: principalId,
+ program_id: programId,
+ role_id: 1,
+ });
+ return true;
+ }
+};
+
+/**
+ * For demonstration purposes only and should be removed from the shipping
+ * product. Enrolls a participant in a program so that the assessments list
+ * feature can be demonstrated.
+ *
+ * @param {number} principalId - The row ID of the principals table
+ * corresponding with the user to enroll as a participant in a program.
+ * @param {number} programId - The row ID of the programs table corresponding
+ * with the program into which the participant should be enrolled.
+ * @returns {Promise} Returns `true` if the user was successfully
+ * enrolled in the program as a participant or if the user was switched to the
+ * participant role; returns `false` if the user was already a participant in
+ * the program.
+ */
+export const enrollParticipant = async (
+ principalId: number,
+ programId: number
+): Promise => {
+ const isEnrolled = await db('program_participants')
+ .where({
+ principal_id: principalId,
+ program_id: programId,
+ })
+ .select('role_id');
+
+ if (isEnrolled.length > 0) {
+ if (isEnrolled[0].role_id !== 2) {
+ await db('program_participants')
+ .where({ principal_id: principalId, program_id: programId })
+ .update({ role_id: 2 });
+ return true;
+ }
+ return false;
+ } else {
+ await db('program_participants').insert({
+ principal_id: principalId,
+ program_id: programId,
+ role_id: 2,
+ });
+ return true;
+ }
+};
+
+/**
+ * Finds a single program assessment by its row ID, if it exists in the
+ * program_assessments table.
+ *
+ * @param {number} programAssessmentId - The row ID of the program_assessments
+ * table for a given program assessment.
+ * @returns {Promise} The ProgramAssessment representation of
+ * that program assessment, or null if no matching program assessment was
+ * found.
+ */
+export const findProgramAssessment = async (
+ programAssessmentId: number
+): Promise => {
+ const matchingProgramAssessmentsRows = await db('program_assessments')
+ .select('program_id', 'assessment_id', 'available_after', 'due_date')
+ .where('id', programAssessmentId);
+
+ if (matchingProgramAssessmentsRows.length === 0) {
+ return null;
+ }
+
+ const [programAssessmentsRow] = matchingProgramAssessmentsRows;
+
+ const program = await findProgram(programAssessmentsRow.program_id);
+
+ const programAssessment: ProgramAssessment = {
+ id: programAssessmentId,
+ program_id: programAssessmentsRow.program_id,
+ assessment_id: programAssessmentsRow.assessment_id,
+ available_after: DateTime.fromSQL(programAssessmentsRow.available_after, {
+ zone: program.time_zone,
+ }).toISO(),
+ due_date: DateTime.fromSQL(programAssessmentsRow.due_date, {
+ zone: program.time_zone,
+ }).toISO(),
+ };
+
+ return programAssessment;
+};
+
+/**
+ * Finds a single program assessment submission by its row ID, if it exists in
+ * the assessment_submissions table. Optionally returns the submission's saved
+ * responses and the grading information for the submission and its responses.
+ *
+ * @param {number} assessmentSubmissionId - The row ID of the
+ * assessment_submissions table for a given program assessment submission.
+ * @param {boolean} [responsesIncluded] - Optional specifier to determine
+ * whether or not the assessment responses will be included in the returned
+ * object.
+ * @param {boolean} [gradingsIncluded] - Optional specifier to override the
+ * default grading information return behavior, such as if a program
+ * facilitator was retrieving the assessment submission. If this parameter is
+ * not specified, the default behavior will take over: the grading information
+ * will only be released if the assessment submission is in the "Graded"
+ * state.
+ * @returns {Promise} The AssessmentSubmission
+ * representation of that program assessment submission, or null if no
+ * matching program assessment submission was found.
+ */
+export const getAssessmentSubmission = async (
+ assessmentSubmissionId: number,
+ responsesIncluded?: boolean,
+ gradingsIncluded?: boolean
+): Promise => {
+ const matchingAssessmentSubmissionsRows = await db('assessment_submissions')
+ .join(
+ 'assessment_submission_states',
+ 'assessment_submissions.assessment_submission_state_id',
+ 'assessment_submission_states.id'
+ )
+ .select(
+ 'assessment_submissions.assessment_id',
+ 'assessment_submissions.principal_id',
+ 'assessment_submission_states.title as assessment_submission_state',
+ 'assessment_submissions.score',
+ 'assessment_submissions.opened_at',
+ 'assessment_submissions.submitted_at',
+ 'assessment_submissions.updated_at'
+ )
+ .where('assessment_submissions.id', assessmentSubmissionId);
+
+ if (matchingAssessmentSubmissionsRows.length === 0) {
+ return null;
+ }
+
+ const [assessmentSubmissionsRow] = matchingAssessmentSubmissionsRows;
+
+ const assessmentSubmission: AssessmentSubmission = {
+ id: assessmentSubmissionId,
+ assessment_id: assessmentSubmissionsRow.assessment_id,
+ principal_id: assessmentSubmissionsRow.principal_id,
+ assessment_submission_state:
+ assessmentSubmissionsRow.assessment_submission_state,
+ opened_at: DateTime.fromSQL(assessmentSubmissionsRow.opened_at, {
+ zone: 'utc',
+ }).toISO(),
+ last_modified: DateTime.fromSQL(assessmentSubmissionsRow.updated_at, {
+ zone: 'utc',
+ }).toISO(),
+ };
+
+ if (assessmentSubmissionsRow.score !== null) {
+ assessmentSubmission.score = assessmentSubmissionsRow.score;
+ }
+
+ if (assessmentSubmissionsRow.submitted_at !== null) {
+ assessmentSubmission.submitted_at = DateTime.fromSQL(
+ assessmentSubmissionsRow.submitted_at,
+ {
+ zone: 'utc',
+ }
+ ).toISO();
+ }
+
+ if (responsesIncluded) {
+ const assessmentResponses = await listSubmissionResponses(
+ assessmentSubmissionId,
+ gradingsIncluded
+ );
+ if (assessmentResponses !== null) {
+ assessmentSubmission.responses = assessmentResponses;
+ }
+ }
+ return assessmentSubmission;
+};
+
+/**
+ * Finds a single curriculum assessment by its row ID, if it exists in the
+ * curriculum_assessments table. Optionally returns the questions and all answer
+ * options, such as when a participant is creating or viewing an assessment
+ * submission, and the questions and correct answers, such as when a participant
+ * is viewing a graded submission or a facilitator is grading a submission.
+ *
+ * @param {number} curriculumAssessmentId - The row ID of the
+ * curriculum_assessments table for a given curriculum assessment.
+ * @param {boolean} [questionsAndAllAnswersIncluded] - Optional specifier to
+ * determine whether or not the questions and all answer options will be
+ * included in the returned object.
+ * @param {boolean} [questionsAndCorrectAnswersIncluded] - Optional specifier to
+ * determine whether or not the correct answer information for the curriculum
+ * assessment questions will be included in the returned object.
+ * @returns {Promise} The CurriculumAssessment
+ * representation of that curriculum assessment, or null if no matching
+ * curriculum assessment was found.
+ */
+export const getCurriculumAssessment = async (
+ curriculumAssessmentId: number,
+ questionsAndAllAnswersIncluded?: boolean,
+ questionsAndCorrectAnswersIncluded?: boolean
+): Promise => {
+ const matchingCurriculumAssessmentRows = await db('curriculum_assessments')
+ .select(
+ 'curriculum_assessments.title',
+ 'curriculum_assessments.max_score',
+ 'curriculum_assessments.max_num_submissions',
+ 'curriculum_assessments.time_limit',
+ 'curriculum_assessments.curriculum_id',
+ 'curriculum_assessments.activity_id',
+ 'curriculum_assessments.principal_id'
+ )
+ .join('activities', 'curriculum_assessments.curriculum_id', 'activities.id')
+ .where('curriculum_assessments.id', curriculumAssessmentId);
+
+ if (matchingCurriculumAssessmentRows.length === 0) {
+ return null;
+ }
+
+ const [matchingCurriculumAssessment] = matchingCurriculumAssessmentRows;
+
+ const [assessmentType] = await db('activity_types')
+ .select('activity_types.title')
+ .join('activities', 'activities.activity_type_id', 'activity_types.id')
+ .where('activities.id', matchingCurriculumAssessment.activity_id);
+
+ const curriculumAssessment: CurriculumAssessment = {
+ id: curriculumAssessmentId,
+ title: matchingCurriculumAssessment.title,
+ assessment_type: assessmentType.title,
+ description: matchingCurriculumAssessment.description,
+ max_score: matchingCurriculumAssessment.max_score,
+ max_num_submissions: matchingCurriculumAssessment.max_num_submissions,
+ time_limit: matchingCurriculumAssessment.time_limit,
+ curriculum_id: matchingCurriculumAssessment.curriculum_id,
+ activity_id: matchingCurriculumAssessment.activity_id,
+ principal_id: matchingCurriculumAssessment.principal_id,
+ };
+
+ if (questionsAndAllAnswersIncluded === true) {
+ curriculumAssessment.questions = await listAssessmentQuestions(
+ curriculumAssessmentId,
+ questionsAndAllAnswersIncluded,
+ questionsAndCorrectAnswersIncluded
+ );
+ }
+
+ return curriculumAssessment;
+};
+
+/**
+ * Retrieves the string representation of a principal's role for a given
+ * program: "Facilitator" for a program facilitator, "Participant" for a program
+ * participant, or null if not enrolled in the specified program.
+ *
+ * @param {number} principalId - The row ID of the principals table that
+ * corresponds with a given program member.
+ * @param {number} programId - The row ID of the programs table for a given
+ * program.
+ * @returns {Promise} The string value of a principal's role in a given
+ * program, or null if they are not enrolled as a participant or facilitating
+ * that program.
+ */
+export const getPrincipalProgramRole = async (
+ principalId: number,
+ programId: number
+): Promise => {
+ const matchingRoleRows = await db('program_participant_roles')
+ .select('program_participant_roles.title')
+ .join(
+ 'program_participants',
+ 'program_participant_roles.id',
+ 'program_participants.role_id'
+ )
+ .where({ principal_id: principalId, program_id: programId });
+
+ if (matchingRoleRows.length === 0) {
+ return null;
+ }
+
+ const [matchingRole] = matchingRoleRows;
+
+ return matchingRole.title;
+};
+
+/**
+ * Lists all submissions by all program participants for a given program
+ * assessment, if any. Does not include responses for those submissions.
+ *
+ * @param {number} programAssessmentId - The row ID of the program_assessments
+ * table for a given program assessment.
+ * @returns {Promise} An array of AssessmentSubmission
+ * objects constructed from matching program assessment submissions, if any,
+ * not including their responses.
+ */
+export const listAllProgramAssessmentSubmissions = async (
+ programAssessmentId: number
+): Promise => {
+ const matchingAssessmentSubmissionsRows = await db('assessment_submissions')
+ .join(
+ 'assessment_submission_states',
+ 'assessment_submissions.assessment_submission_state_id',
+ 'assessment_submission_states.id'
+ )
+ .select(
+ 'assessment_submissions.id',
+ 'assessment_submission_states.title as assessment_submission_state',
+ 'assessment_submissions.principal_id',
+ 'assessment_submissions.score',
+ 'assessment_submissions.opened_at',
+ 'assessment_submissions.submitted_at',
+ 'assessment_submissions.updated_at'
+ )
+ .where('assessment_id', programAssessmentId);
+
+ if (matchingAssessmentSubmissionsRows.length === 0) {
+ return null;
+ }
+
+ const assessmentSubmissions: AssessmentSubmission[] = [];
+
+ for (const assessmentSubmissionsRow of matchingAssessmentSubmissionsRows) {
+ const assessmentSubmission: AssessmentSubmission = {
+ id: assessmentSubmissionsRow.id,
+ assessment_id: programAssessmentId,
+ principal_id: assessmentSubmissionsRow.principal_id,
+ assessment_submission_state:
+ assessmentSubmissionsRow.assessment_submission_state,
+ opened_at: DateTime.fromSQL(assessmentSubmissionsRow.opened_at, {
+ zone: 'utc',
+ }).toISO(),
+ last_modified: DateTime.fromSQL(assessmentSubmissionsRow.updated_at, {
+ zone: 'utc',
+ }).toISO(),
+ };
+
+ if (assessmentSubmissionsRow.score !== null) {
+ assessmentSubmission.score = assessmentSubmissionsRow.score;
+ }
+
+ if (assessmentSubmissionsRow.submitted_at !== null) {
+ assessmentSubmission.submitted_at = DateTime.fromSQL(
+ assessmentSubmissionsRow.submitted_at,
+ { zone: 'utc' }
+ ).toISO();
+ }
+
+ assessmentSubmissions.push(assessmentSubmission);
+ }
+
+ return assessmentSubmissions;
+};
+
+/**
+ * Lists all submissions by a program participant for a given program
+ * assessment, if any. Does not include responses for those submissions.
+ *
+ * @param {number} participantPrincipalId - The row ID of the principals table
+ * that corresponds with a given program participant.
+ * @param {number} programAssessmentId - The row ID of the program_assessments
+ * table for a given program assessment.
+ * @returns {Promise} An array of AssessmentSubmission
+ * objects constructed from matching program assessment submissions, if any,
+ * not including their responses.
+ */
+export const listParticipantProgramAssessmentSubmissions = async (
+ participantPrincipalId: number,
+ programAssessmentId: number
+): Promise => {
+ const matchingAssessmentSubmissionsRows = await db('assessment_submissions')
+ .join(
+ 'assessment_submission_states',
+ 'assessment_submissions.assessment_submission_state_id',
+ 'assessment_submission_states.id'
+ )
+ .select(
+ 'assessment_submissions.id as id',
+ 'assessment_submission_states.title as assessment_submission_state',
+ 'assessment_submissions.score',
+ 'assessment_submissions.opened_at',
+ 'assessment_submissions.submitted_at',
+ 'assessment_submissions.updated_at'
+ )
+ .where('assessment_submissions.principal_id', participantPrincipalId)
+ .andWhere('assessment_submissions.assessment_id', programAssessmentId);
+
+ if (matchingAssessmentSubmissionsRows.length === 0) {
+ return null;
+ }
+
+ const assessmentSubmissions: AssessmentSubmission[] = [];
+
+ for (const assessmentSubmissionsRow of matchingAssessmentSubmissionsRows) {
+ const assessmentSubmission: AssessmentSubmission = {
+ id: assessmentSubmissionsRow.id,
+ assessment_id: programAssessmentId,
+ principal_id: participantPrincipalId,
+ assessment_submission_state:
+ assessmentSubmissionsRow.assessment_submission_state,
+ opened_at: DateTime.fromSQL(assessmentSubmissionsRow.opened_at, {
+ zone: 'utc',
+ }).toISO(),
+ last_modified: DateTime.fromSQL(assessmentSubmissionsRow.updated_at, {
+ zone: 'utc',
+ }).toISO(),
+ };
+
+ if (assessmentSubmissionsRow.score !== null) {
+ assessmentSubmission.score = assessmentSubmissionsRow.score;
+ }
+
+ if (assessmentSubmissionsRow.submitted_at !== null) {
+ assessmentSubmission.submitted_at = DateTime.fromSQL(
+ assessmentSubmissionsRow.submitted_at,
+ { zone: 'utc' }
+ ).toISO();
+ }
+
+ assessmentSubmissions.push(assessmentSubmission);
+ }
+
+ return assessmentSubmissions;
+};
+
+/**
+ * Lists all row IDs of programs for which a principal is either enrolled as a
+ * participant or is designated as facilitator.
+ *
+ * @param {number} principalId - The row ID of the principals table that
+ * corresponds with a given program member.
+ * @returns {Promise} An array of row IDs for all matching programs
+ * for which the user is enrolled or is facilitating.
+ */
+export const listPrincipalEnrolledProgramIds = async (
+ principalId: number
+): Promise => {
+ const enrolledProgramsList = await db('program_participants')
+ .select('program_id')
+ .where({ principal_id: principalId });
+
+ if (enrolledProgramsList.length === 0) {
+ return null;
+ }
+
+ const programList: number[] = enrolledProgramsList.map(
+ enrolledProgram => enrolledProgram.program_id
+ );
+
+ return programList;
+};
+
+/**
+ * Lists all available program assessments for a given program.
+ *
+ * @param {number} programId - The row ID of the programs table for a given
+ * program.
+ * @returns {Promise} An array of the ProgramAssessment
+ * objects constructed from matching program assessments, if any.
+ */
+export const listProgramAssessments = async (
+ programId: number
+): Promise => {
+ const matchingProgramAssessmentsRows = await db('program_assessments')
+ .select('id', 'assessment_id', 'available_after', 'due_date')
+ .where('program_id', programId);
+
+ if (matchingProgramAssessmentsRows.length === 0) {
+ return null;
+ }
+
+ const programAssessments: ProgramAssessment[] = [];
+
+ for (const programAssessmentsRow of matchingProgramAssessmentsRows) {
+ const program = await findProgram(programId);
+
+ programAssessments.push({
+ id: programAssessmentsRow.id,
+ program_id: programId,
+ assessment_id: programAssessmentsRow.assessment_id,
+ available_after: DateTime.fromSQL(programAssessmentsRow.available_after, {
+ zone: program.time_zone,
+ }).toISO(),
+ due_date: DateTime.fromSQL(programAssessmentsRow.due_date, {
+ zone: program.time_zone,
+ }).toISO(),
+ });
+ }
+
+ return programAssessments;
+};
+
+/**
+ * Retrieves all program IDs that a given user is a facilitator for, matching a
+ * given curriculum ID. This is used for routes where we have a curriculum
+ * assessment ID and do not have a way to check if a user is allowed to make
+ * edits to that curriculum assessment.
+ *
+ * @param {number} principalId - The row ID of the principals table for the
+ * logged-in user.
+ * @param {number} curriculumId - The row ID of the curriculums table
+ * corresponding to the curriculum assessment we will be retrieving,
+ * modifying, or deleting.
+ * @returns {Promise} An array of the row IDs of the programs table
+ * matching the given curriculum ID.
+ */
+export const facilitatorProgramIdsMatchingCurriculum = async (
+ principalId: number,
+ curriculumId: number
+): Promise => {
+ const participatingProgramIds = await listPrincipalEnrolledProgramIds(
+ principalId
+ );
+
+ if (participatingProgramIds === null) {
+ return [];
+ }
+
+ const curriculumPrograms = await listProgramsForCurriculum(curriculumId);
+
+ const matchingFacilitatorPrograms: number[] = [];
+
+ for (const programId of participatingProgramIds) {
+ const programRole = await getPrincipalProgramRole(principalId, programId);
+
+ if (programRole === 'Facilitator') {
+ if (
+ curriculumPrograms.filter(program => program.id === programId)
+ .length !== 0
+ ) {
+ matchingFacilitatorPrograms.push(programId);
+ }
+ }
+ }
+
+ return matchingFacilitatorPrograms;
+};
+
+/**
+ * Removes any possible grading information from an assessment submission in
+ * cases when we don't want to return that information to the requester.
+ *
+ * @param {AssessmentSubmission} assessmentSubmissionWithGrades - An
+ * AssessmentSubmission possibly containing grading information.
+ * @returns {AssessmentSubmission} The updated object with any possible grading
+ * information removed, without harming the original object.
+ */
+export const removeGradingInformation = (
+ assessmentSubmissionWithGrades: AssessmentSubmission
+): AssessmentSubmission => {
+ const gradeRemovedSubmission = { ...assessmentSubmissionWithGrades };
+ delete gradeRemovedSubmission.score;
+
+ if (assessmentSubmissionWithGrades.responses) {
+ const gradeRemovedResponses = assessmentSubmissionWithGrades.responses.map(
+ response => {
+ const gradeRemovedResponse = structuredClone(response);
+ delete gradeRemovedResponse.score;
+ delete gradeRemovedResponse.grader_response;
+ return gradeRemovedResponse;
+ }
+ );
+ gradeRemovedSubmission.responses = gradeRemovedResponses;
+ }
+
+ return gradeRemovedSubmission;
+};
+
+/**
+ * Updates a program assessment submission for a program participant, if their
+ * time has not expired, or updates a program assessment submission by the
+ * facilitator, if optional parameter passed is true. If the submission is
+ * expired and the function is not passed true for facilitatorOverride, the only
+ * update allowed will be to mark a program assessment submission "Expired"
+ * instead of "Opened" or "In Progress".
+ *
+ * @param {AssessmentSubmission} assessmentSubmission - The updated program
+ * assessment submission information, including responses.
+ * @param {boolean} [facilitatorOverride] - Optional specifier for when the
+ * program facilitator is the one updating a program assessment submission,
+ * skipping the submission expiration check.
+ * @returns {Promise} An AssessmentSubmission object
+ * constructed from the updated row in the assessment_submissions table.
+ */
+export const updateAssessmentSubmission = async (
+ assessmentSubmission: AssessmentSubmission,
+ facilitatorOverride?: boolean
+): Promise => {
+ const existingAssessmentSubmission = await getAssessmentSubmission(
+ assessmentSubmission.id,
+ true,
+ facilitatorOverride || false
+ );
+
+ const updatedSubmission = structuredClone(assessmentSubmission);
+
+ let newState;
+
+ if (facilitatorOverride && facilitatorOverride === true) {
+ const updatedResponses: AssessmentResponse[] = [];
+
+ for (const assessmentResponse of assessmentSubmission.responses) {
+ updatedResponses.push(
+ await updateSubmissionResponse(assessmentResponse, true)
+ );
+ }
+
+ newState = assessmentSubmission.assessment_submission_state;
+ const [gradedStateId] = await db('assessment_submission_states')
+ .select('id')
+ .where('title', newState);
+ await db('assessment_submissions')
+ .update({
+ assessment_submission_state_id: gradedStateId.id,
+ score: assessmentSubmission.score,
+ })
+ .where('id', assessmentSubmission.id);
+
+ updatedSubmission.responses = updatedResponses;
+ updatedSubmission.assessment_submission_state = newState;
+ } else if (
+ ['Expired', 'Submitted', 'Graded'].includes(
+ existingAssessmentSubmission.assessment_submission_state
+ )
+ ) {
+ return existingAssessmentSubmission;
+ } else if (
+ ['Opened', 'In Progress'].includes(
+ existingAssessmentSubmission.assessment_submission_state
+ )
+ ) {
+ const assessmentSubmissionNoGrades =
+ removeGradingInformation(assessmentSubmission);
+
+ if (
+ assessmentSubmission.assessment_submission_state === 'Expired' ||
+ (await assessmentSubmissionExpired(assessmentSubmission))
+ ) {
+ newState = 'Expired';
+
+ updatedSubmission.last_modified = DateTime.now()
+ .plus({ weeks: 1 })
+ .toUTC()
+ .toISO();
+ } else {
+ newState =
+ assessmentSubmission.assessment_submission_state === 'Submitted'
+ ? 'Submitted'
+ : 'In Progress';
+
+ const updatedResponses: AssessmentResponse[] = [];
+
+ if (
+ assessmentSubmission.responses &&
+ Array.isArray(assessmentSubmission.responses) &&
+ assessmentSubmission.responses.length > 0
+ ) {
+ for (const assessmentResponse of assessmentSubmissionNoGrades.responses) {
+ const matchingExistingResponses =
+ existingAssessmentSubmission.responses?.filter(
+ e => e.id === assessmentResponse.id
+ );
+
+ if (
+ !Array.isArray(existingAssessmentSubmission.responses) ||
+ matchingExistingResponses.length === 0
+ ) {
+ updatedResponses.push(
+ await createSubmissionResponse(assessmentResponse)
+ );
+ } else {
+ const [existingResponse] = matchingExistingResponses;
+
+ if (
+ (typeof assessmentResponse.answer_id !== 'undefined' &&
+ assessmentResponse.answer_id !== null &&
+ existingResponse.answer_id !== assessmentResponse.answer_id) ||
+ (typeof assessmentResponse.response_text !== 'undefined' &&
+ assessmentResponse.response_text !== null &&
+ existingResponse.response_text !==
+ assessmentResponse.response_text)
+ ) {
+ updatedResponses.push(
+ await updateSubmissionResponse(assessmentResponse, false)
+ );
+ } else {
+ updatedResponses.push(assessmentResponse);
+ }
+ }
+ }
+ }
+
+ updatedSubmission.responses = updatedResponses;
+ }
+
+ const [newStateId] = await db('assessment_submission_states')
+ .select('id')
+ .where('title', newState);
+
+ if (newState === 'Submitted') {
+ await db('assessment_submissions')
+ .update({
+ assessment_submission_state_id: newStateId.id,
+ submitted_at: db.fn.now(),
+ })
+ .where('id', assessmentSubmission.id);
+ } else {
+ await db('assessment_submissions')
+ .update({ assessment_submission_state_id: newStateId.id })
+ .where('id', assessmentSubmission.id);
+ }
+
+ updatedSubmission.assessment_submission_state = newState;
+ }
+
+ return updatedSubmission;
+};
+
+/**
+ * Updates an existing curriculum assessment, its metadata, and its associated
+ * questions and answers if given.
+ *
+ * @param {CurriculumAssessment} curriculumAssessment - The updated curriculum
+ * assessment information with which to update the corresponding database
+ * data.
+ * @returns {Promise} The updated CurriculumAssessment
+ * object that was handed to us, if update was successful.
+ */
+export const updateCurriculumAssessment = async (
+ curriculumAssessment: CurriculumAssessment
+): Promise => {
+ const existingQuestions = await listAssessmentQuestions(
+ curriculumAssessment.id,
+ false,
+ false
+ );
+
+ const newQuestions: Question[] = [];
+ const updatedQuestions: Question[] = [];
+
+ if (
+ curriculumAssessment.questions &&
+ Array.isArray(curriculumAssessment.questions) &&
+ curriculumAssessment.questions.length > 0
+ ) {
+ for (const question of curriculumAssessment.questions) {
+ if (question.id && typeof question.id === 'number' && question.id > 0) {
+ if (
+ existingQuestions &&
+ Array.isArray(existingQuestions) &&
+ existingQuestions.length > 0
+ ) {
+ const eqIndex = existingQuestions.findIndex(
+ existingQuestion => existingQuestion.id === question.id
+ );
+ existingQuestions.splice(eqIndex, 1);
+ }
+ updatedQuestions.push(question);
+ } else {
+ newQuestions.push(question);
+ }
+ }
+ }
+
+ if (
+ existingQuestions &&
+ Array.isArray(existingQuestions) &&
+ existingQuestions.length > 0
+ ) {
+ for (const deletedQuestion of existingQuestions) {
+ await deleteAssessmentQuestion(deletedQuestion.id);
+ }
+ }
+
+ const newQuestionList: Question[] = [];
+
+ for (const updatedQuestion of updatedQuestions) {
+ newQuestionList.push(await updateAssessmentQuestion(updatedQuestion));
+ }
+
+ for (const newQuestion of newQuestions) {
+ newQuestionList.push(
+ await createAssessmentQuestion(curriculumAssessment.id, newQuestion)
+ );
+ }
+
+ await db('curriculum_assessments')
+ .update({
+ title: curriculumAssessment.title,
+ description: curriculumAssessment.description,
+ max_score: curriculumAssessment.max_score,
+ max_num_submissions: curriculumAssessment.max_num_submissions,
+ time_limit: curriculumAssessment.time_limit,
+ activity_id: curriculumAssessment.activity_id,
+ })
+ .where('id', curriculumAssessment.id);
+
+ curriculumAssessment.questions = newQuestionList;
+
+ return curriculumAssessment;
+};
+
+/**
+ * Updates an existing program assessment in the program_assessments table.
+ *
+ * @param {ProgramAssessment} programAssessment - The updated program assessment
+ * information with which to update the corresponding database data.
+ * @returns {Promise} The updated ProgramAssessment object
+ * that was handed to us, if update was successful.
+ */
+export const updateProgramAssessment = async (
+ programAssessment: ProgramAssessment
+): Promise => {
+ await db('program_assessments')
+ .update({
+ available_after: programAssessment.available_after,
+ due_date: programAssessment.due_date,
+ })
+ .where('id', programAssessment.id);
+
+ return programAssessment;
+};
diff --git a/api/src/services/db.ts b/api/src/services/db.ts
index 1ea08373..a6b10fca 100644
--- a/api/src/services/db.ts
+++ b/api/src/services/db.ts
@@ -10,7 +10,7 @@ const tzOffset = new IANAZone(
).formatOffset(DateTime.now().toMillis(), 'short');
const db = knex({
- client: 'mysql',
+ client: 'mysql2',
connection: {
database: findConfig('MYSQL_DATABASE', ''),
host: findConfig('MYSQL_HOST', 'localhost'),
diff --git a/api/src/services/questionnairesService.ts b/api/src/services/questionnairesService.ts
index 98e4cc23..29ab1a43 100644
--- a/api/src/services/questionnairesService.ts
+++ b/api/src/services/questionnairesService.ts
@@ -1,13 +1,13 @@
import db from './db';
export const findQuestionnaire = async (questionnaireId: number) => {
- const [questionnaire] = await db('questionnaires')
+ const [questionnaire] = await db('surveys')
.select('id')
.where({ id: questionnaireId });
if (!questionnaire) {
return null;
}
- const prompts = await db('prompts')
+ const prompts = await db('survey_questions')
.select('id', 'label', 'query_text')
.where({
questionnaire_id: questionnaireId,
@@ -17,7 +17,7 @@ export const findQuestionnaire = async (questionnaireId: number) => {
const promptsById = new Map();
let options;
if (promptIds.length) {
- options = await db('options')
+ options = await db('survey_answers')
.select('id', 'label', 'prompt_id')
.whereIn('prompt_id', promptIds)
.orderBy('prompt_id', 'sort_order');
diff --git a/api/src/services/reflectionsService.ts b/api/src/services/reflectionsService.ts
index 806e0541..81c0bd9b 100644
--- a/api/src/services/reflectionsService.ts
+++ b/api/src/services/reflectionsService.ts
@@ -33,14 +33,18 @@ export const listReflections = async (
id: 'responses.id',
reflection_id: 'reflection_id',
option_id: 'option_id',
- option_label: 'options.label',
+ option_label: 'survey_answers.label',
prompt_id: 'prompt_id',
- prompt_label: 'prompts.label',
+ prompt_label: 'survey_questions.label',
})
- .join('options', 'responses.option_id', 'options.id')
- .join('prompts', 'options.prompt_id', 'prompts.id')
+ .join('survey_answers', 'responses.option_id', 'survey_answers.id')
+ .join(
+ 'survey_questions',
+ 'survey_answers.prompt_id',
+ 'survey_questions.id'
+ )
.whereIn('reflection_id', reflectionIds)
- .orderBy('prompts.sort_order'),
+ .orderBy('survey_questions.sort_order'),
]);
const reflectionsById = new Map();
for (const reflection of reflections) {
diff --git a/api/yarn.lock b/api/yarn.lock
index 1326bed9..7180309c 100644
--- a/api/yarn.lock
+++ b/api/yarn.lock
@@ -2,79 +2,80 @@
# yarn lockfile v1
-"@ampproject/remapping@^2.1.0":
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
- integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
+"@ampproject/remapping@^2.2.0":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630"
+ integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==
dependencies:
- "@jridgewell/gen-mapping" "^0.1.0"
+ "@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
- integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.21.4":
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39"
+ integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==
dependencies:
"@babel/highlight" "^7.18.6"
-"@babel/compat-data@^7.20.5":
- version "7.20.14"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8"
- integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==
+"@babel/compat-data@^7.21.5":
+ version "7.21.9"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.9.tgz#10a2e7fda4e51742c907938ac3b7229426515514"
+ integrity sha512-FUGed8kfhyWvbYug/Un/VPJD41rDIgoVVcR+FuzhzOYyRz5uED+Gd3SLZml0Uw2l2aHFb7ZgdW5mGA3G2cCCnQ==
"@babel/core@^7.11.6", "@babel/core@^7.12.3":
- version "7.20.12"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d"
- integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==
- dependencies:
- "@ampproject/remapping" "^2.1.0"
- "@babel/code-frame" "^7.18.6"
- "@babel/generator" "^7.20.7"
- "@babel/helper-compilation-targets" "^7.20.7"
- "@babel/helper-module-transforms" "^7.20.11"
- "@babel/helpers" "^7.20.7"
- "@babel/parser" "^7.20.7"
+ version "7.21.8"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.8.tgz#2a8c7f0f53d60100ba4c32470ba0281c92aa9aa4"
+ integrity sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.21.4"
+ "@babel/generator" "^7.21.5"
+ "@babel/helper-compilation-targets" "^7.21.5"
+ "@babel/helper-module-transforms" "^7.21.5"
+ "@babel/helpers" "^7.21.5"
+ "@babel/parser" "^7.21.8"
"@babel/template" "^7.20.7"
- "@babel/traverse" "^7.20.12"
- "@babel/types" "^7.20.7"
+ "@babel/traverse" "^7.21.5"
+ "@babel/types" "^7.21.5"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.2"
semver "^6.3.0"
-"@babel/generator@^7.20.7", "@babel/generator@^7.7.2":
- version "7.20.14"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.14.tgz#9fa772c9f86a46c6ac9b321039400712b96f64ce"
- integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==
+"@babel/generator@^7.21.5", "@babel/generator@^7.7.2":
+ version "7.21.9"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.9.tgz#3a1b706e07d836e204aee0650e8ee878d3aaa241"
+ integrity sha512-F3fZga2uv09wFdEjEQIJxXALXfz0+JaOb7SabvVMmjHxeVTuGW8wgE8Vp1Hd7O+zMTYtcfEISGRzPkeiaPPsvg==
dependencies:
- "@babel/types" "^7.20.7"
+ "@babel/types" "^7.21.5"
"@jridgewell/gen-mapping" "^0.3.2"
+ "@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
-"@babel/helper-compilation-targets@^7.20.7":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb"
- integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==
+"@babel/helper-compilation-targets@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.5.tgz#631e6cc784c7b660417421349aac304c94115366"
+ integrity sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==
dependencies:
- "@babel/compat-data" "^7.20.5"
- "@babel/helper-validator-option" "^7.18.6"
+ "@babel/compat-data" "^7.21.5"
+ "@babel/helper-validator-option" "^7.21.0"
browserslist "^4.21.3"
lru-cache "^5.1.1"
semver "^6.3.0"
-"@babel/helper-environment-visitor@^7.18.9":
- version "7.18.9"
- resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
- integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
+"@babel/helper-environment-visitor@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.21.5.tgz#c769afefd41d171836f7cb63e295bedf689d48ba"
+ integrity sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==
-"@babel/helper-function-name@^7.19.0":
- version "7.19.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c"
- integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==
+"@babel/helper-function-name@^7.21.0":
+ version "7.21.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4"
+ integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==
dependencies:
- "@babel/template" "^7.18.10"
- "@babel/types" "^7.19.0"
+ "@babel/template" "^7.20.7"
+ "@babel/types" "^7.21.0"
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
@@ -83,38 +84,38 @@
dependencies:
"@babel/types" "^7.18.6"
-"@babel/helper-module-imports@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
- integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
+"@babel/helper-module-imports@^7.21.4":
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz#ac88b2f76093637489e718a90cec6cf8a9b029af"
+ integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==
dependencies:
- "@babel/types" "^7.18.6"
+ "@babel/types" "^7.21.4"
-"@babel/helper-module-transforms@^7.20.11":
- version "7.20.11"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0"
- integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==
+"@babel/helper-module-transforms@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.5.tgz#d937c82e9af68d31ab49039136a222b17ac0b420"
+ integrity sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==
dependencies:
- "@babel/helper-environment-visitor" "^7.18.9"
- "@babel/helper-module-imports" "^7.18.6"
- "@babel/helper-simple-access" "^7.20.2"
+ "@babel/helper-environment-visitor" "^7.21.5"
+ "@babel/helper-module-imports" "^7.21.4"
+ "@babel/helper-simple-access" "^7.21.5"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/helper-validator-identifier" "^7.19.1"
"@babel/template" "^7.20.7"
- "@babel/traverse" "^7.20.10"
- "@babel/types" "^7.20.7"
+ "@babel/traverse" "^7.21.5"
+ "@babel/types" "^7.21.5"
-"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.8.0":
- version "7.20.2"
- resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629"
- integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.21.5.tgz#345f2377d05a720a4e5ecfa39cbf4474a4daed56"
+ integrity sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==
-"@babel/helper-simple-access@^7.20.2":
- version "7.20.2"
- resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9"
- integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==
+"@babel/helper-simple-access@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.21.5.tgz#d697a7971a5c39eac32c7e63c0921c06c8a249ee"
+ integrity sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==
dependencies:
- "@babel/types" "^7.20.2"
+ "@babel/types" "^7.21.5"
"@babel/helper-split-export-declaration@^7.18.6":
version "7.18.6"
@@ -123,29 +124,29 @@
dependencies:
"@babel/types" "^7.18.6"
-"@babel/helper-string-parser@^7.19.4":
- version "7.19.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
- integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
+"@babel/helper-string-parser@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.21.5.tgz#2b3eea65443c6bdc31c22d037c65f6d323b6b2bd"
+ integrity sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==
"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
-"@babel/helper-validator-option@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
- integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==
+"@babel/helper-validator-option@^7.21.0":
+ version "7.21.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180"
+ integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==
-"@babel/helpers@^7.20.7":
- version "7.20.13"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.13.tgz#e3cb731fb70dc5337134cadc24cbbad31cc87ad2"
- integrity sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==
+"@babel/helpers@^7.21.5":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.5.tgz#5bac66e084d7a4d2d9696bdf0175a93f7fb63c08"
+ integrity sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==
dependencies:
"@babel/template" "^7.20.7"
- "@babel/traverse" "^7.20.13"
- "@babel/types" "^7.20.7"
+ "@babel/traverse" "^7.21.5"
+ "@babel/types" "^7.21.5"
"@babel/highlight@^7.18.6":
version "7.18.6"
@@ -156,10 +157,10 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.13", "@babel/parser@^7.20.7":
- version "7.20.15"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89"
- integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.5", "@babel/parser@^7.21.8", "@babel/parser@^7.21.9":
+ version "7.21.9"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.9.tgz#ab18ea3b85b4bc33ba98a8d4c2032c557d23cf14"
+ integrity sha512-q5PNg/Bi1OpGgx5jYlvWZwAorZepEudDMCLtj967aeS7WMont7dUZI46M2XwcIQqvUlMxWfdLFu4S/qSxeUu5g==
"@babel/plugin-syntax-async-generators@^7.8.4":
version "7.8.4"
@@ -197,11 +198,11 @@
"@babel/helper-plugin-utils" "^7.8.0"
"@babel/plugin-syntax-jsx@^7.7.2":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
- integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.21.4.tgz#f264ed7bf40ffc9ec239edabc17a50c4f5b6fea2"
+ integrity sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==
dependencies:
- "@babel/helper-plugin-utils" "^7.18.6"
+ "@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-logical-assignment-operators@^7.8.3":
version "7.10.4"
@@ -253,43 +254,43 @@
"@babel/helper-plugin-utils" "^7.14.5"
"@babel/plugin-syntax-typescript@^7.7.2":
- version "7.20.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7"
- integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.19.0"
-
-"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
- integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==
- dependencies:
- "@babel/code-frame" "^7.18.6"
- "@babel/parser" "^7.20.7"
- "@babel/types" "^7.20.7"
-
-"@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.7.2":
- version "7.20.13"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473"
- integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==
- dependencies:
- "@babel/code-frame" "^7.18.6"
- "@babel/generator" "^7.20.7"
- "@babel/helper-environment-visitor" "^7.18.9"
- "@babel/helper-function-name" "^7.19.0"
+ version "7.21.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.21.4.tgz#2751948e9b7c6d771a8efa59340c15d4a2891ff8"
+ integrity sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.20.2"
+
+"@babel/template@^7.20.7", "@babel/template@^7.3.3":
+ version "7.21.9"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.21.9.tgz#bf8dad2859130ae46088a99c1f265394877446fb"
+ integrity sha512-MK0X5k8NKOuWRamiEfc3KEJiHMTkGZNUjzMipqCGDDc6ijRl/B7RGSKVGncu4Ro/HdyzzY6cmoXuKI2Gffk7vQ==
+ dependencies:
+ "@babel/code-frame" "^7.21.4"
+ "@babel/parser" "^7.21.9"
+ "@babel/types" "^7.21.5"
+
+"@babel/traverse@^7.21.5", "@babel/traverse@^7.7.2":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.5.tgz#ad22361d352a5154b498299d523cf72998a4b133"
+ integrity sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==
+ dependencies:
+ "@babel/code-frame" "^7.21.4"
+ "@babel/generator" "^7.21.5"
+ "@babel/helper-environment-visitor" "^7.21.5"
+ "@babel/helper-function-name" "^7.21.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
- "@babel/parser" "^7.20.13"
- "@babel/types" "^7.20.7"
+ "@babel/parser" "^7.21.5"
+ "@babel/types" "^7.21.5"
debug "^4.1.0"
globals "^11.1.0"
-"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3":
- version "7.20.7"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f"
- integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==
+"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.4", "@babel/types@^7.21.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3":
+ version "7.21.5"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.5.tgz#18dfbd47c39d3904d5db3d3dc2cc80bedb60e5b6"
+ integrity sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==
dependencies:
- "@babel/helper-string-parser" "^7.19.4"
+ "@babel/helper-string-parser" "^7.21.5"
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
@@ -305,14 +306,26 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
-"@eslint/eslintrc@^1.4.1":
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e"
- integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==
+"@eslint-community/eslint-utils@^4.2.0":
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
+ integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
+ dependencies:
+ eslint-visitor-keys "^3.3.0"
+
+"@eslint-community/regexpp@^4.4.0":
+ version "4.5.1"
+ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884"
+ integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==
+
+"@eslint/eslintrc@^2.0.3":
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331"
+ integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
- espree "^9.4.0"
+ espree "^9.5.2"
globals "^13.19.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
@@ -320,6 +333,11 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
+"@eslint/js@8.41.0":
+ version "8.41.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3"
+ integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==
+
"@humanwhocodes/config-array@^0.11.8":
version "0.11.8"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@@ -355,109 +373,109 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
-"@jest/console@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.4.2.tgz#f78374905c2454764152904a344a2d5226b0ef09"
- integrity sha512-0I/rEJwMpV9iwi9cDEnT71a5nNGK9lj8Z4+1pRAU2x/thVXCDnaTGrvxyK+cAqZTFVFCiR+hfVrP4l2m+dCmQg==
+"@jest/console@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.5.0.tgz#593a6c5c0d3f75689835f1b3b4688c4f8544cb57"
+ integrity sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==
dependencies:
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
chalk "^4.0.0"
- jest-message-util "^29.4.2"
- jest-util "^29.4.2"
+ jest-message-util "^29.5.0"
+ jest-util "^29.5.0"
slash "^3.0.0"
-"@jest/core@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.4.2.tgz#6e999b67bdc2df9d96ba9b142465bda71ee472c2"
- integrity sha512-KGuoQah0P3vGNlaS/l9/wQENZGNKGoWb+OPxh3gz+YzG7/XExvYu34MzikRndQCdM2S0tzExN4+FL37i6gZmCQ==
+"@jest/core@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.5.0.tgz#76674b96904484e8214614d17261cc491e5f1f03"
+ integrity sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==
dependencies:
- "@jest/console" "^29.4.2"
- "@jest/reporters" "^29.4.2"
- "@jest/test-result" "^29.4.2"
- "@jest/transform" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/console" "^29.5.0"
+ "@jest/reporters" "^29.5.0"
+ "@jest/test-result" "^29.5.0"
+ "@jest/transform" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
ansi-escapes "^4.2.1"
chalk "^4.0.0"
ci-info "^3.2.0"
exit "^0.1.2"
graceful-fs "^4.2.9"
- jest-changed-files "^29.4.2"
- jest-config "^29.4.2"
- jest-haste-map "^29.4.2"
- jest-message-util "^29.4.2"
- jest-regex-util "^29.4.2"
- jest-resolve "^29.4.2"
- jest-resolve-dependencies "^29.4.2"
- jest-runner "^29.4.2"
- jest-runtime "^29.4.2"
- jest-snapshot "^29.4.2"
- jest-util "^29.4.2"
- jest-validate "^29.4.2"
- jest-watcher "^29.4.2"
+ jest-changed-files "^29.5.0"
+ jest-config "^29.5.0"
+ jest-haste-map "^29.5.0"
+ jest-message-util "^29.5.0"
+ jest-regex-util "^29.4.3"
+ jest-resolve "^29.5.0"
+ jest-resolve-dependencies "^29.5.0"
+ jest-runner "^29.5.0"
+ jest-runtime "^29.5.0"
+ jest-snapshot "^29.5.0"
+ jest-util "^29.5.0"
+ jest-validate "^29.5.0"
+ jest-watcher "^29.5.0"
micromatch "^4.0.4"
- pretty-format "^29.4.2"
+ pretty-format "^29.5.0"
slash "^3.0.0"
strip-ansi "^6.0.0"
-"@jest/environment@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.4.2.tgz#ee92c316ee2fbdf0bcd9d2db0ef42d64fea26b56"
- integrity sha512-JKs3VUtse0vQfCaFGJRX1bir9yBdtasxziSyu+pIiEllAQOe4oQhdCYIf3+Lx+nGglFktSKToBnRJfD5QKp+NQ==
+"@jest/environment@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.5.0.tgz#9152d56317c1fdb1af389c46640ba74ef0bb4c65"
+ integrity sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==
dependencies:
- "@jest/fake-timers" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/fake-timers" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
- jest-mock "^29.4.2"
+ jest-mock "^29.5.0"
-"@jest/expect-utils@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.4.2.tgz#cd0065dfdd8e8a182aa350cc121db97b5eed7b3f"
- integrity sha512-Dd3ilDJpBnqa0GiPN7QrudVs0cczMMHtehSo2CSTjm3zdHx0RcpmhFNVEltuEFeqfLIyWKFI224FsMSQ/nsJQA==
+"@jest/expect-utils@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036"
+ integrity sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==
dependencies:
- jest-get-type "^29.4.2"
+ jest-get-type "^29.4.3"
-"@jest/expect@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.4.2.tgz#2d4a6a41b29380957c5094de19259f87f194578b"
- integrity sha512-NUAeZVApzyaeLjfWIV/64zXjA2SS+NuUPHpAlO7IwVMGd5Vf9szTl9KEDlxY3B4liwLO31os88tYNHl6cpjtKQ==
+"@jest/expect@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.5.0.tgz#80952f5316b23c483fbca4363ce822af79c38fba"
+ integrity sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==
dependencies:
- expect "^29.4.2"
- jest-snapshot "^29.4.2"
+ expect "^29.5.0"
+ jest-snapshot "^29.5.0"
-"@jest/fake-timers@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.4.2.tgz#af43ee1a5720b987d0348f80df98f2cb17d45cd0"
- integrity sha512-Ny1u0Wg6kCsHFWq7A/rW/tMhIedq2siiyHyLpHCmIhP7WmcAmd2cx95P+0xtTZlj5ZbJxIRQi4OPydZZUoiSQQ==
+"@jest/fake-timers@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.5.0.tgz#d4d09ec3286b3d90c60bdcd66ed28d35f1b4dc2c"
+ integrity sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==
dependencies:
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
"@sinonjs/fake-timers" "^10.0.2"
"@types/node" "*"
- jest-message-util "^29.4.2"
- jest-mock "^29.4.2"
- jest-util "^29.4.2"
+ jest-message-util "^29.5.0"
+ jest-mock "^29.5.0"
+ jest-util "^29.5.0"
-"@jest/globals@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.4.2.tgz#73f85f5db0e17642258b25fd0b9fc89ddedb50eb"
- integrity sha512-zCk70YGPzKnz/I9BNFDPlK+EuJLk21ur/NozVh6JVM86/YYZtZHqxFFQ62O9MWq7uf3vIZnvNA0BzzrtxD9iyg==
+"@jest/globals@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.5.0.tgz#6166c0bfc374c58268677539d0c181f9c1833298"
+ integrity sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==
dependencies:
- "@jest/environment" "^29.4.2"
- "@jest/expect" "^29.4.2"
- "@jest/types" "^29.4.2"
- jest-mock "^29.4.2"
+ "@jest/environment" "^29.5.0"
+ "@jest/expect" "^29.5.0"
+ "@jest/types" "^29.5.0"
+ jest-mock "^29.5.0"
-"@jest/reporters@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.4.2.tgz#6abfa923941daae0acc76a18830ee9e79a22042d"
- integrity sha512-10yw6YQe75zCgYcXgEND9kw3UZZH5tJeLzWv4vTk/2mrS1aY50A37F+XT2hPO5OqQFFnUWizXD8k1BMiATNfUw==
+"@jest/reporters@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.5.0.tgz#985dfd91290cd78ddae4914ba7921bcbabe8ac9b"
+ integrity sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==
dependencies:
"@bcoe/v8-coverage" "^0.2.3"
- "@jest/console" "^29.4.2"
- "@jest/test-result" "^29.4.2"
- "@jest/transform" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/console" "^29.5.0"
+ "@jest/test-result" "^29.5.0"
+ "@jest/transform" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@jridgewell/trace-mapping" "^0.3.15"
"@types/node" "*"
chalk "^4.0.0"
@@ -470,115 +488,117 @@
istanbul-lib-report "^3.0.0"
istanbul-lib-source-maps "^4.0.0"
istanbul-reports "^3.1.3"
- jest-message-util "^29.4.2"
- jest-util "^29.4.2"
- jest-worker "^29.4.2"
+ jest-message-util "^29.5.0"
+ jest-util "^29.5.0"
+ jest-worker "^29.5.0"
slash "^3.0.0"
string-length "^4.0.1"
strip-ansi "^6.0.0"
v8-to-istanbul "^9.0.1"
-"@jest/schemas@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.2.tgz#cf7cfe97c5649f518452b176c47ed07486270fc1"
- integrity sha512-ZrGzGfh31NtdVH8tn0mgJw4khQuNHiKqdzJAFbCaERbyCP9tHlxWuL/mnMu8P7e/+k4puWjI1NOzi/sFsjce/g==
+"@jest/schemas@^29.4.3":
+ version "29.4.3"
+ resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788"
+ integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==
dependencies:
"@sinclair/typebox" "^0.25.16"
-"@jest/source-map@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.2.tgz#f9815d59e25cd3d6828e41489cd239271018d153"
- integrity sha512-tIoqV5ZNgYI9XCKXMqbYe5JbumcvyTgNN+V5QW4My033lanijvCD0D4PI9tBw4pRTqWOc00/7X3KVvUh+qnF4Q==
+"@jest/source-map@^29.4.3":
+ version "29.4.3"
+ resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.3.tgz#ff8d05cbfff875d4a791ab679b4333df47951d20"
+ integrity sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==
dependencies:
"@jridgewell/trace-mapping" "^0.3.15"
callsites "^3.0.0"
graceful-fs "^4.2.9"
-"@jest/test-result@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.4.2.tgz#34b0ba069f2e3072261e4884c8fb6bd15ed6fb8d"
- integrity sha512-HZsC3shhiHVvMtP+i55MGR5bPcc3obCFbA5bzIOb8pCjwBZf11cZliJncCgaVUbC5yoQNuGqCkC0Q3t6EItxZA==
+"@jest/test-result@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.5.0.tgz#7c856a6ca84f45cc36926a4e9c6b57f1973f1408"
+ integrity sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==
dependencies:
- "@jest/console" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/console" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/istanbul-lib-coverage" "^2.0.0"
collect-v8-coverage "^1.0.0"
-"@jest/test-sequencer@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.4.2.tgz#8b48e5bc4af80b42edacaf2a733d4f295edf28fb"
- integrity sha512-9Z2cVsD6CcObIVrWigHp2McRJhvCxL27xHtrZFgNC1RwnoSpDx6fZo8QYjJmziFlW9/hr78/3sxF54S8B6v8rg==
+"@jest/test-sequencer@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.5.0.tgz#34d7d82d3081abd523dbddc038a3ddcb9f6d3cc4"
+ integrity sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==
dependencies:
- "@jest/test-result" "^29.4.2"
+ "@jest/test-result" "^29.5.0"
graceful-fs "^4.2.9"
- jest-haste-map "^29.4.2"
+ jest-haste-map "^29.5.0"
slash "^3.0.0"
-"@jest/transform@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.4.2.tgz#b24b72dbab4c8675433a80e222d6a8ef4656fb81"
- integrity sha512-kf1v5iTJHn7p9RbOsBuc/lcwyPtJaZJt5885C98omWz79NIeD3PfoiiaPSu7JyCyFzNOIzKhmMhQLUhlTL9BvQ==
+"@jest/transform@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.5.0.tgz#cf9c872d0965f0cbd32f1458aa44a2b1988b00f9"
+ integrity sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==
dependencies:
"@babel/core" "^7.11.6"
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
"@jridgewell/trace-mapping" "^0.3.15"
babel-plugin-istanbul "^6.1.1"
chalk "^4.0.0"
convert-source-map "^2.0.0"
fast-json-stable-stringify "^2.1.0"
graceful-fs "^4.2.9"
- jest-haste-map "^29.4.2"
- jest-regex-util "^29.4.2"
- jest-util "^29.4.2"
+ jest-haste-map "^29.5.0"
+ jest-regex-util "^29.4.3"
+ jest-util "^29.5.0"
micromatch "^4.0.4"
pirates "^4.0.4"
slash "^3.0.0"
write-file-atomic "^4.0.2"
-"@jest/types@^29.4.2":
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.2.tgz#8f724a414b1246b2bfd56ca5225d9e1f39540d82"
- integrity sha512-CKlngyGP0fwlgC1BRUtPZSiWLBhyS9dKwKmyGxk8Z6M82LBEGB2aLQSg+U1MyLsU+M7UjnlLllBM2BLWKVm/Uw==
+"@jest/types@^29.5.0":
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593"
+ integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==
dependencies:
- "@jest/schemas" "^29.4.2"
+ "@jest/schemas" "^29.4.3"
"@types/istanbul-lib-coverage" "^2.0.0"
"@types/istanbul-reports" "^3.0.0"
"@types/node" "*"
"@types/yargs" "^17.0.8"
chalk "^4.0.0"
-"@jridgewell/gen-mapping@^0.1.0":
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
- integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
- dependencies:
- "@jridgewell/set-array" "^1.0.0"
- "@jridgewell/sourcemap-codec" "^1.4.10"
-
-"@jridgewell/gen-mapping@^0.3.2":
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
- integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2":
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098"
+ integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==
dependencies:
"@jridgewell/set-array" "^1.0.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
-"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3":
+"@jridgewell/resolve-uri@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
-"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
+"@jridgewell/resolve-uri@^3.0.3":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
+ integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+
+"@jridgewell/set-array@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
-"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
+"@jridgewell/sourcemap-codec@1.4.14":
version "1.4.14"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+"@jridgewell/sourcemap-codec@^1.4.10":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
@@ -587,10 +607,10 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
-"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9":
- version "0.3.17"
- resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
- integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
+"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.18"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6"
+ integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==
dependencies:
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
@@ -616,24 +636,58 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@redis/bloom@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71"
+ integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==
+
+"@redis/client@1.5.7":
+ version "1.5.7"
+ resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.7.tgz#92cc5c98c76f189e37d24f0e1e17e104c6af17d4"
+ integrity sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==
+ dependencies:
+ cluster-key-slot "1.1.2"
+ generic-pool "3.9.0"
+ yallist "4.0.0"
+
+"@redis/graph@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519"
+ integrity sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==
+
+"@redis/json@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1"
+ integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==
+
+"@redis/search@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.2.tgz#6a8f66ba90812d39c2457420f859ce8fbd8f3838"
+ integrity sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==
+
+"@redis/time-series@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.4.tgz#af85eb080f6934580e4d3b58046026b6c2b18717"
+ integrity sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==
+
"@sinclair/typebox@^0.25.16":
- version "0.25.21"
- resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272"
- integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g==
+ version "0.25.24"
+ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718"
+ integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==
-"@sinonjs/commons@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
- integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==
+"@sinonjs/commons@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72"
+ integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==
dependencies:
type-detect "4.0.8"
"@sinonjs/fake-timers@^10.0.2":
- version "10.0.2"
- resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c"
- integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==
+ version "10.2.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz#b3e322a34c5f26e3184e7f6115695f299c1b1194"
+ integrity sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==
dependencies:
- "@sinonjs/commons" "^2.0.0"
+ "@sinonjs/commons" "^3.0.0"
"@socket.io/component-emitter@~3.1.0":
version "3.1.0"
@@ -656,9 +710,9 @@
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
- integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
+ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/babel__core@^7.1.14":
version "7.20.0"
@@ -687,9 +741,9 @@
"@babel/types" "^7.0.0"
"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6":
- version "7.18.3"
- resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d"
- integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==
+ version "7.18.5"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.5.tgz#c107216842905afafd3b6e774f6f935da6f5db80"
+ integrity sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q==
dependencies:
"@babel/types" "^7.3.0"
@@ -701,16 +755,6 @@
"@types/connect" "*"
"@types/node" "*"
-"@types/connect-redis@^0.0.19":
- version "0.0.19"
- resolved "https://registry.yarnpkg.com/@types/connect-redis/-/connect-redis-0.0.19.tgz#0895bbb1f2e3b5d2695158ec3f3ce367d1fca455"
- integrity sha512-2312okmqA8MtogPkLmgwmM12VeSYH8gUAuRSzAtVz3PBoyEZwqt7Ri1lXBFtJmIVd3oXC/Hvg1KJSkt9x2ukKw==
- dependencies:
- "@types/express" "*"
- "@types/express-session" "*"
- "@types/ioredis" "^4.28.10"
- "@types/redis" "^2.8.0"
-
"@types/connect@*":
version "3.4.35"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
@@ -736,18 +780,19 @@
"@types/node" "*"
"@types/express-serve-static-core@^4.17.33":
- version "4.17.33"
- resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543"
- integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==
+ version "4.17.35"
+ resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f"
+ integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
+ "@types/send" "*"
-"@types/express-session@*", "@types/express-session@^1.17.4":
- version "1.17.5"
- resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.5.tgz#13f48852b4aa60ff595835faeb4b4dda0ba0866e"
- integrity sha512-l0DhkvNVfyUPEEis8fcwbd46VptfA/jmMwHfob2TfDMf3HyPLiB9mKD71LXhz5TMUobODXPD27zXSwtFQLHm+w==
+"@types/express-session@^1.17.7":
+ version "1.17.7"
+ resolved "https://registry.yarnpkg.com/@types/express-session/-/express-session-1.17.7.tgz#ced215c1244cb594be10e39f2781ddcd650be9a6"
+ integrity sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==
dependencies:
"@types/express" "*"
@@ -768,13 +813,6 @@
dependencies:
"@types/node" "*"
-"@types/ioredis@^4.28.10":
- version "4.28.10"
- resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff"
- integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==
- dependencies:
- "@types/node" "*"
-
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
@@ -794,10 +832,10 @@
dependencies:
"@types/istanbul-lib-report" "*"
-"@types/jest@^29.4.0":
- version "29.4.0"
- resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.0.tgz#a8444ad1704493e84dbf07bb05990b275b3b9206"
- integrity sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==
+"@types/jest@^29.5.1":
+ version "29.5.1"
+ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47"
+ integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==
dependencies:
expect "^29.0.0"
pretty-format "^29.0.0"
@@ -807,20 +845,25 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
-"@types/luxon@^3.2.0":
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.2.0.tgz#99901b4ab29a5fdffc88fff59b3b47fbfbe0557b"
- integrity sha512-lGmaGFoaXHuOLXFvuju2bfvZRqxAqkHPx9Y9IQdQABrinJJshJwfNCKV+u7rR3kJbiqfTF/NhOkcxxAFrObyaA==
+"@types/luxon@^3.3.0":
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.0.tgz#a61043a62c0a72696c73a0a305c544c96501e006"
+ integrity sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==
"@types/mime@*":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10"
integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==
-"@types/mock-knex@^0.4.3":
- version "0.4.4"
- resolved "https://registry.yarnpkg.com/@types/mock-knex/-/mock-knex-0.4.4.tgz#3631f9995eded35f763cc1b387ffc7e385f3f4a5"
- integrity sha512-IbRF/6EKVZ9iHIu0e7xFR0dwBmJJ53hS6+mLp/EuXxRJ1828sxJ3pA7akl5ex4gC6x9BQ1R4QbXiSVurucHhbw==
+"@types/mime@^1":
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
+ integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
+
+"@types/mock-knex@^0.4.5":
+ version "0.4.5"
+ resolved "https://registry.yarnpkg.com/@types/mock-knex/-/mock-knex-0.4.5.tgz#33df948a75b3d70d26f1cb9baa6fd11f8e228d8d"
+ integrity sha512-vWDRgK86CnM7cISJI7eA7avW4VB/+s9Ux0AaAXIsYYkabbUgyLVxrnAyBWBWD6MAacZPDpCCd6VDf8L1ajbFdg==
dependencies:
"@types/node" "*"
@@ -831,10 +874,10 @@
dependencies:
"@types/node" "*"
-"@types/node@*", "@types/node@>=10.0.0", "@types/node@^18.13.0":
- version "18.13.0"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850"
- integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==
+"@types/node@*", "@types/node@>=10.0.0", "@types/node@^20.2.3":
+ version "20.2.3"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.3.tgz#b31eb300610c3835ac008d690de6f87e28f9b878"
+ integrity sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==
"@types/prettier@^2.1.5":
version "2.7.2"
@@ -851,22 +894,23 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
-"@types/redis@^2.8.0":
- version "2.8.32"
- resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11"
- integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==
+"@types/semver@^7.3.12":
+ version "7.5.0"
+ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
+ integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==
+
+"@types/send@*":
+ version "0.17.1"
+ resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301"
+ integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==
dependencies:
+ "@types/mime" "^1"
"@types/node" "*"
-"@types/semver@^7.3.12":
- version "7.3.13"
- resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
- integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
-
"@types/serve-static@*":
- version "1.15.0"
- resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155"
- integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==
+ version "1.15.1"
+ resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d"
+ integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==
dependencies:
"@types/mime" "*"
"@types/node" "*"
@@ -876,10 +920,10 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
-"@types/superagent@*", "@types/superagent@^4.1.13":
- version "4.1.16"
- resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.16.tgz#12c9c16f232f9d89beab91d69368f96ce8e2d881"
- integrity sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==
+"@types/superagent@*", "@types/superagent@^4.1.17":
+ version "4.1.17"
+ resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.17.tgz#c8f0162b5d8a9c52d38b81398ef0650ef974b452"
+ integrity sha512-FFK/rRjNy24U6J1BvQkaNWu2ohOIF/kxRQXRsbT141YQODcOcZjzlcc4DGdI2SkTa0rhmF+X14zu6ICjCGIg+w==
dependencies:
"@types/cookiejar" "*"
"@types/node" "*"
@@ -897,94 +941,94 @@
integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==
"@types/yargs@^17.0.8":
- version "17.0.22"
- resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a"
- integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g==
+ version "17.0.24"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902"
+ integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==
dependencies:
"@types/yargs-parser" "*"
-"@typescript-eslint/eslint-plugin@^5.51.0":
- version "5.51.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.51.0.tgz#da3f2819633061ced84bb82c53bba45a6fe9963a"
- integrity sha512-wcAwhEWm1RgNd7dxD/o+nnLW8oH+6RK1OGnmbmkj/GGoDPV1WWMVP0FXYQBivKHdwM1pwii3bt//RC62EriIUQ==
+"@typescript-eslint/eslint-plugin@^5.59.7":
+ version "5.59.7"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz#e470af414f05ecfdc05a23e9ce6ec8f91db56fe2"
+ integrity sha512-BL+jYxUFIbuYwy+4fF86k5vdT9lT0CNJ6HtwrIvGh0PhH8s0yy5rjaKH2fDCrz5ITHy07WCzVGNvAmjJh4IJFA==
dependencies:
- "@typescript-eslint/scope-manager" "5.51.0"
- "@typescript-eslint/type-utils" "5.51.0"
- "@typescript-eslint/utils" "5.51.0"
+ "@eslint-community/regexpp" "^4.4.0"
+ "@typescript-eslint/scope-manager" "5.59.7"
+ "@typescript-eslint/type-utils" "5.59.7"
+ "@typescript-eslint/utils" "5.59.7"
debug "^4.3.4"
grapheme-splitter "^1.0.4"
ignore "^5.2.0"
natural-compare-lite "^1.4.0"
- regexpp "^3.2.0"
semver "^7.3.7"
tsutils "^3.21.0"
-"@typescript-eslint/parser@^5.51.0":
- version "5.51.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.51.0.tgz#2d74626652096d966ef107f44b9479f02f51f271"
- integrity sha512-fEV0R9gGmfpDeRzJXn+fGQKcl0inIeYobmmUWijZh9zA7bxJ8clPhV9up2ZQzATxAiFAECqPQyMDB4o4B81AaA==
+"@typescript-eslint/parser@^5.59.7":
+ version "5.59.7"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.7.tgz#02682554d7c1028b89aa44a48bf598db33048caa"
+ integrity sha512-VhpsIEuq/8i5SF+mPg9jSdIwgMBBp0z9XqjiEay+81PYLJuroN+ET1hM5IhkiYMJd9MkTz8iJLt7aaGAgzWUbQ==
dependencies:
- "@typescript-eslint/scope-manager" "5.51.0"
- "@typescript-eslint/types" "5.51.0"
- "@typescript-eslint/typescript-estree" "5.51.0"
+ "@typescript-eslint/scope-manager" "5.59.7"
+ "@typescript-eslint/types" "5.59.7"
+ "@typescript-eslint/typescript-estree" "5.59.7"
debug "^4.3.4"
-"@typescript-eslint/scope-manager@5.51.0":
- version "5.51.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.51.0.tgz#ad3e3c2ecf762d9a4196c0fbfe19b142ac498990"
- integrity sha512-gNpxRdlx5qw3yaHA0SFuTjW4rxeYhpHxt491PEcKF8Z6zpq0kMhe0Tolxt0qjlojS+/wArSDlj/LtE69xUJphQ==
+"@typescript-eslint/scope-manager@5.59.7":
+ version "5.59.7"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz#0243f41f9066f3339d2f06d7f72d6c16a16769e2"
+ integrity sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==
dependencies:
- "@typescript-eslint/types" "5.51.0"
- "@typescript-eslint/visitor-keys" "5.51.0"
+ "@typescript-eslint/types" "5.59.7"
+ "@typescript-eslint/visitor-keys" "5.59.7"
-"@typescript-eslint/type-utils@5.51.0":
- version "5.51.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.51.0.tgz#7af48005531700b62a20963501d47dfb27095988"
- integrity sha512-QHC5KKyfV8sNSyHqfNa0UbTbJ6caB8uhcx2hYcWVvJAZYJRBo5HyyZfzMdRx8nvS+GyMg56fugMzzWnojREuQQ==
+"@typescript-eslint/type-utils@5.59.7":
+ version "5.59.7"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz#89c97291371b59eb18a68039857c829776f1426d"
+ integrity sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ==
dependencies:
- "@typescript-eslint/typescript-estree" "5.51.0"
- "@typescript-eslint/utils" "5.51.0"
+ "@typescript-eslint/typescript-estree" "5.59.7"
+ "@typescript-eslint/utils" "5.59.7"
debug "^4.3.4"
tsutils "^3.21.0"
-"@typescript-eslint/types@5.51.0":
- version "5.51.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.51.0.tgz#e7c1622f46c7eea7e12bbf1edfb496d4dec37c90"
- integrity sha512-SqOn0ANn/v6hFn0kjvLwiDi4AzR++CBZz0NV5AnusT2/3y32jdc0G4woXPWHCumWtUXZKPAS27/9vziSsC9jnw==
+"@typescript-eslint/types@5.59.7":
+ version "5.59.7"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.7.tgz#6f4857203fceee91d0034ccc30512d2939000742"
+ integrity sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==
-"@typescript-eslint/typescript-estree@5.51.0":
- version "5.51.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.51.0.tgz#0ec8170d7247a892c2b21845b06c11eb0718f8de"
- integrity sha512-TSkNupHvNRkoH9FMA3w7TazVFcBPveAAmb7Sz+kArY6sLT86PA5Vx80cKlYmd8m3Ha2SwofM1KwraF24lM9FvA==
+"@typescript-eslint/typescript-estree@5.59.7":
+ version "5.59.7"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz#b887acbd4b58e654829c94860dbff4ac55c5cff8"
+ integrity sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==
dependencies:
- "@typescript-eslint/types" "5.51.0"
- "@typescript-eslint/visitor-keys" "5.51.0"
+ "@typescript-eslint/types" "5.59.7"
+ "@typescript-eslint/visitor-keys" "5.59.7"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
-"@typescript-eslint/utils@5.51.0":
- version "5.51.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.51.0.tgz#074f4fabd5b12afe9c8aa6fdee881c050f8b4d47"
- integrity sha512-76qs+5KWcaatmwtwsDJvBk4H76RJQBFe+Gext0EfJdC3Vd2kpY2Pf//OHHzHp84Ciw0/rYoGTDnIAr3uWhhJYw==
+"@typescript-eslint/utils@5.59.7":
+ version "5.59.7"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.7.tgz#7adf068b136deae54abd9a66ba5a8780d2d0f898"
+ integrity sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ==
dependencies:
+ "@eslint-community/eslint-utils" "^4.2.0"
"@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12"
- "@typescript-eslint/scope-manager" "5.51.0"
- "@typescript-eslint/types" "5.51.0"
- "@typescript-eslint/typescript-estree" "5.51.0"
+ "@typescript-eslint/scope-manager" "5.59.7"
+ "@typescript-eslint/types" "5.59.7"
+ "@typescript-eslint/typescript-estree" "5.59.7"
eslint-scope "^5.1.1"
- eslint-utils "^3.0.0"
semver "^7.3.7"
-"@typescript-eslint/visitor-keys@5.51.0":
- version "5.51.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.51.0.tgz#c0147dd9a36c0de758aaebd5b48cae1ec59eba87"
- integrity sha512-Oh2+eTdjHjOFjKA27sxESlA87YPSOJafGCR0md5oeMdh1ZcCfAGCIOL216uTBAkAIptvLIfKQhl7lHxMJet4GQ==
+"@typescript-eslint/visitor-keys@5.59.7":
+ version "5.59.7"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz#09c36eaf268086b4fbb5eb9dc5199391b6485fc5"
+ integrity sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==
dependencies:
- "@typescript-eslint/types" "5.51.0"
+ "@typescript-eslint/types" "5.59.7"
eslint-visitor-keys "^3.3.0"
abbrev@1:
@@ -1106,15 +1150,15 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
-babel-jest@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.4.2.tgz#b17b9f64be288040877cbe2649f91ac3b63b2ba6"
- integrity sha512-vcghSqhtowXPG84posYkkkzcZsdayFkubUgbE3/1tuGbX7AQtwCkkNA/wIbB0BMjuCPoqTkiDyKN7Ty7d3uwNQ==
+babel-jest@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.5.0.tgz#3fe3ddb109198e78b1c88f9ebdecd5e4fc2f50a5"
+ integrity sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==
dependencies:
- "@jest/transform" "^29.4.2"
+ "@jest/transform" "^29.5.0"
"@types/babel__core" "^7.1.14"
babel-plugin-istanbul "^6.1.1"
- babel-preset-jest "^29.4.2"
+ babel-preset-jest "^29.5.0"
chalk "^4.0.0"
graceful-fs "^4.2.9"
slash "^3.0.0"
@@ -1130,10 +1174,10 @@ babel-plugin-istanbul@^6.1.1:
istanbul-lib-instrument "^5.0.4"
test-exclude "^6.0.0"
-babel-plugin-jest-hoist@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.2.tgz#22aa43e255230f02371ffef1cac7eedef58f60bc"
- integrity sha512-5HZRCfMeWypFEonRbEkwWXtNS1sQK159LhRVyRuLzyfVBxDy/34Tr/rg4YVi0SScSJ4fqeaR/OIeceJ/LaQ0pQ==
+babel-plugin-jest-hoist@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz#a97db437936f441ec196990c9738d4b88538618a"
+ integrity sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==
dependencies:
"@babel/template" "^7.3.3"
"@babel/types" "^7.3.3"
@@ -1158,12 +1202,12 @@ babel-preset-current-node-syntax@^1.0.0:
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
"@babel/plugin-syntax-top-level-await" "^7.8.3"
-babel-preset-jest@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.4.2.tgz#f0b20c6a79a9f155515e72a2d4f537fe002a4e38"
- integrity sha512-ecWdaLY/8JyfUDr0oELBMpj3R5I1L6ZqG+kRJmwqfHtLWuPrJStR0LUkvUhfykJWTsXXMnohsayN/twltBbDrQ==
+babel-preset-jest@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz#57bc8cc88097af7ff6a5ab59d1cd29d52a5916e2"
+ integrity sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==
dependencies:
- babel-plugin-jest-hoist "^29.4.2"
+ babel-plugin-jest-hoist "^29.5.0"
babel-preset-current-node-syntax "^1.0.0"
balanced-match@^1.0.0:
@@ -1176,11 +1220,6 @@ base64id@2.0.0, base64id@~2.0.0:
resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
-bignumber.js@9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075"
- integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==
-
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@@ -1282,9 +1321,9 @@ camelcase@^6.2.0:
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001449:
- version "1.0.30001451"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz#2e197c698fc1373d63e1406d6607ea4617c613f1"
- integrity sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w==
+ version "1.0.30001489"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001489.tgz#ca82ee2d4e4dbf2bd2589c9360d3fcc2c7ba3bd8"
+ integrity sha512-x1mgZEXK8jHIfAxm+xgdpHpk50IN3z3q3zP261/WS+uvePxW8izXuCu6AHz0lkuYTlATDehiZ/tNyYBdSQsOUQ==
chalk@^2.0.0:
version "2.4.2"
@@ -1324,9 +1363,9 @@ chokidar@^3.5.2:
fsevents "~2.3.2"
ci-info@^3.2.0:
- version "3.7.1"
- resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f"
- integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91"
+ integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==
cjs-module-lexer@^1.0.0:
version "1.2.2"
@@ -1342,6 +1381,11 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
+cluster-key-slot@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
+ integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -1403,10 +1447,10 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
-connect-redis@^6.0.0:
- version "6.1.3"
- resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-6.1.3.tgz#0a83c953f9ece45ae37d304a8e8d1c3c6a60b4b9"
- integrity sha512-aaNluLlAn/3JPxRwdzw7lhvEoU6Enb+d83xnokUNhC9dktqBoawKWL+WuxinxvBLTz6q9vReTnUDnUslaz74aw==
+connect-redis@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-7.1.0.tgz#6618334697f2ed91536848cdc03734def2138acf"
+ integrity sha512-UaqO1EirWjON2ENsyau7N5lbkrdYBpS6mYlXSeff/OYXsd6EGZ+SXSmNPoljL2PSua8fgjAEaldSA73PMZQ9Eg==
console-polyfill@0.3.0:
version "0.3.0"
@@ -1455,11 +1499,6 @@ cookiejar@^2.1.4:
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b"
integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==
-core-util-is@~1.0.0:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
- integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
-
cors@^2.8.5, cors@~2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
@@ -1521,19 +1560,19 @@ deep-is@^0.1.3:
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
deepmerge@^4.2.2:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b"
- integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
+ integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
-denque@^1.5.0:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf"
- integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==
+denque@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1"
+ integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==
depd@2.0.0, depd@~2.0.0:
version "2.0.0"
@@ -1558,10 +1597,10 @@ dezalgo@^1.0.4:
asap "^2.0.0"
wrappy "1"
-diff-sequences@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.2.tgz#711fe6bd8a5869fe2539cee4a5152425ff671fda"
- integrity sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw==
+diff-sequences@^29.4.3:
+ version "29.4.3"
+ resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2"
+ integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==
diff@^4.0.1:
version "4.0.2"
@@ -1593,9 +1632,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.4.284:
- version "1.4.294"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.294.tgz#ad80317b85f0859a9454680fbc1c726fefa7e6fd"
- integrity sha512-PuHZB3jEN7D8WPPjLmBQAsqQz8tWHlkkB4n0E2OYw8RwVdmBYV0Wn+rUFH8JqYyIRb4HQhhedgxlZL163wqLrQ==
+ version "1.4.402"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.402.tgz#9aa7bbb63081513127870af6d22f829344c5ba57"
+ integrity sha512-gWYvJSkohOiBE6ecVYXkrDgNaUjo47QEKK0kQzmWyhkH+yoYiG44bwuicTGNSIQRG3WDMsWVZJLRnJnLNkbWvA==
emittery@^0.13.1:
version "0.13.1"
@@ -1617,10 +1656,10 @@ engine.io-parser@~5.0.3:
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45"
integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==
-engine.io@~6.4.0:
- version "6.4.0"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.0.tgz#de27f79ecb58301171aea7956f3f6f4fa578490a"
- integrity sha512-OgxY1c/RuCSeO/rTr8DIFXx76IzUUft86R7/P7MMbbkuzeqJoTNw2lmeD91IyGz41QYleIIjWeMJGgug043sfQ==
+engine.io@~6.4.1:
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.2.tgz#ffeaf68f69b1364b0286badddf15ff633476473f"
+ integrity sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==
dependencies:
"@types/cookie" "^0.4.1"
"@types/cors" "^2.8.12"
@@ -1672,10 +1711,10 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-eslint-config-prettier@^8.6.0:
- version "8.6.0"
- resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207"
- integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==
+eslint-config-prettier@^8.8.0:
+ version "8.8.0"
+ resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
+ integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
eslint-scope@^5.1.1:
version "5.1.1"
@@ -1685,37 +1724,28 @@ eslint-scope@^5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
-eslint-scope@^7.1.1:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642"
- integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==
+eslint-scope@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
+ integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
-eslint-utils@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
- integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
- dependencies:
- eslint-visitor-keys "^2.0.0"
-
-eslint-visitor-keys@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
- integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
-
-eslint-visitor-keys@^3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
- integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
+ integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
-eslint@^8.33.0:
- version "8.33.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.33.0.tgz#02f110f32998cb598c6461f24f4d306e41ca33d7"
- integrity sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==
+eslint@^8.41.0:
+ version "8.41.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c"
+ integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==
dependencies:
- "@eslint/eslintrc" "^1.4.1"
+ "@eslint-community/eslint-utils" "^4.2.0"
+ "@eslint-community/regexpp" "^4.4.0"
+ "@eslint/eslintrc" "^2.0.3"
+ "@eslint/js" "8.41.0"
"@humanwhocodes/config-array" "^0.11.8"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
@@ -1725,24 +1755,22 @@ eslint@^8.33.0:
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
- eslint-scope "^7.1.1"
- eslint-utils "^3.0.0"
- eslint-visitor-keys "^3.3.0"
- espree "^9.4.0"
- esquery "^1.4.0"
+ eslint-scope "^7.2.0"
+ eslint-visitor-keys "^3.4.1"
+ espree "^9.5.2"
+ esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.19.0"
- grapheme-splitter "^1.0.4"
+ graphemer "^1.4.0"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
- js-sdsl "^4.1.4"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
@@ -1750,7 +1778,6 @@ eslint@^8.33.0:
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.1"
- regexpp "^3.2.0"
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
@@ -1760,24 +1787,24 @@ esm@^3.2.25:
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
-espree@^9.4.0:
- version "9.4.1"
- resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd"
- integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==
+espree@^9.5.2:
+ version "9.5.2"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b"
+ integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==
dependencies:
acorn "^8.8.0"
acorn-jsx "^5.3.2"
- eslint-visitor-keys "^3.3.0"
+ eslint-visitor-keys "^3.4.1"
esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-esquery@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
- integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
+esquery@^1.4.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
+ integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
dependencies:
estraverse "^5.1.0"
@@ -1828,16 +1855,16 @@ exit@^0.1.2:
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==
-expect@^29.0.0, expect@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.2.tgz#2ae34eb88de797c64a1541ad0f1e2ea8a7a7b492"
- integrity sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ==
+expect@^29.0.0, expect@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-29.5.0.tgz#68c0509156cb2a0adb8865d413b137eeaae682f7"
+ integrity sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==
dependencies:
- "@jest/expect-utils" "^29.4.2"
- jest-get-type "^29.4.2"
- jest-matcher-utils "^29.4.2"
- jest-message-util "^29.4.2"
- jest-util "^29.4.2"
+ "@jest/expect-utils" "^29.5.0"
+ jest-get-type "^29.4.3"
+ jest-matcher-utils "^29.5.0"
+ jest-message-util "^29.5.0"
+ jest-util "^29.5.0"
express-session@^1.17.2:
version "1.17.3"
@@ -2042,6 +2069,18 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+generate-function@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
+ integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
+ dependencies:
+ is-property "^1.0.2"
+
+generic-pool@3.9.0:
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4"
+ integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==
+
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -2053,12 +2092,13 @@ get-caller-file@^2.0.5:
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.0.2:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f"
- integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
+ integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
+ has-proto "^1.0.1"
has-symbols "^1.0.3"
get-package-type@^0.1.0:
@@ -2127,15 +2167,20 @@ globby@^11.1.0:
slash "^3.0.0"
graceful-fs@^4.2.9:
- version "4.2.10"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
- integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
+ version "4.2.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
grapheme-splitter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
+graphemer@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
+ integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -2146,6 +2191,11 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+has-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
+ integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==
+
has-symbols@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
@@ -2158,10 +2208,10 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
-helmet@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/helmet/-/helmet-6.0.1.tgz#52ec353638b2e87f14fe079d142b368ac11e79a4"
- integrity sha512-8wo+VdQhTMVBMCITYZaGTbE4lvlthelPYSvoyNvk4RECTmrVjMerp9RfUOQXZWLvCcAn1pKj7ZRxK4lI9Alrcw==
+helmet@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.0.0.tgz#ac3011ba82fa2467f58075afa58a49427ba6212d"
+ integrity sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==
hexoid@^1.0.0:
version "1.0.0"
@@ -2196,6 +2246,13 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
+iconv-lite@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
ignore-by-default@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
@@ -2235,7 +2292,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.4, inherits@~2.0.3:
+inherits@2, inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -2262,10 +2319,10 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
-is-core-module@^2.9.0:
- version "2.11.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
- integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==
+is-core-module@^2.11.0:
+ version "2.12.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.12.1.tgz#0c0b6885b6f80011c71541ce15c8d66cf5a4f9fd"
+ integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==
dependencies:
has "^1.0.3"
@@ -2301,6 +2358,11 @@ is-path-inside@^3.0.3:
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
+is-property@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+ integrity sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==
+
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@@ -2311,11 +2373,6 @@ is_js@^0.9.0:
resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d"
integrity sha512-8Y5EHSH+TonfUHX2g3pMJljdbGavg55q4jmHzghJCdqYDbdNROC8uw/YFQwIRCRqRJT1EY3pJefz+kglw+o7sg==
-isarray@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
- integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
-
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -2363,284 +2420,284 @@ istanbul-reports@^3.1.3:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
-jest-changed-files@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.2.tgz#bee1fafc8b620d6251423d1978a0080546bc4376"
- integrity sha512-Qdd+AXdqD16PQa+VsWJpxR3kN0JyOCX1iugQfx5nUgAsI4gwsKviXkpclxOK9ZnwaY2IQVHz+771eAvqeOlfuw==
+jest-changed-files@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.5.0.tgz#e88786dca8bf2aa899ec4af7644e16d9dcf9b23e"
+ integrity sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==
dependencies:
execa "^5.0.0"
p-limit "^3.1.0"
-jest-circus@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.4.2.tgz#2d00c04baefd0ee2a277014cd494d4b5970663ed"
- integrity sha512-wW3ztp6a2P5c1yOc1Cfrt5ozJ7neWmqeXm/4SYiqcSriyisgq63bwFj1NuRdSR5iqS0CMEYwSZd89ZA47W9zUg==
+jest-circus@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.5.0.tgz#b5926989449e75bff0d59944bae083c9d7fb7317"
+ integrity sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==
dependencies:
- "@jest/environment" "^29.4.2"
- "@jest/expect" "^29.4.2"
- "@jest/test-result" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/environment" "^29.5.0"
+ "@jest/expect" "^29.5.0"
+ "@jest/test-result" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
chalk "^4.0.0"
co "^4.6.0"
dedent "^0.7.0"
is-generator-fn "^2.0.0"
- jest-each "^29.4.2"
- jest-matcher-utils "^29.4.2"
- jest-message-util "^29.4.2"
- jest-runtime "^29.4.2"
- jest-snapshot "^29.4.2"
- jest-util "^29.4.2"
+ jest-each "^29.5.0"
+ jest-matcher-utils "^29.5.0"
+ jest-message-util "^29.5.0"
+ jest-runtime "^29.5.0"
+ jest-snapshot "^29.5.0"
+ jest-util "^29.5.0"
p-limit "^3.1.0"
- pretty-format "^29.4.2"
+ pretty-format "^29.5.0"
+ pure-rand "^6.0.0"
slash "^3.0.0"
stack-utils "^2.0.3"
-jest-cli@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.4.2.tgz#94a2f913a0a7a49d11bee98ad88bf48baae941f4"
- integrity sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q==
+jest-cli@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.5.0.tgz#b34c20a6d35968f3ee47a7437ff8e53e086b4a67"
+ integrity sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==
dependencies:
- "@jest/core" "^29.4.2"
- "@jest/test-result" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/core" "^29.5.0"
+ "@jest/test-result" "^29.5.0"
+ "@jest/types" "^29.5.0"
chalk "^4.0.0"
exit "^0.1.2"
graceful-fs "^4.2.9"
import-local "^3.0.2"
- jest-config "^29.4.2"
- jest-util "^29.4.2"
- jest-validate "^29.4.2"
+ jest-config "^29.5.0"
+ jest-util "^29.5.0"
+ jest-validate "^29.5.0"
prompts "^2.0.1"
yargs "^17.3.1"
-jest-config@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.4.2.tgz#15386dd9ed2f7059516915515f786b8836a98f07"
- integrity sha512-919CtnXic52YM0zW4C1QxjG6aNueX1kBGthuMtvFtRTAxhKfJmiXC9qwHmi6o2josjbDz8QlWyY55F1SIVmCWA==
+jest-config@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.5.0.tgz#3cc972faec8c8aaea9ae158c694541b79f3748da"
+ integrity sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==
dependencies:
"@babel/core" "^7.11.6"
- "@jest/test-sequencer" "^29.4.2"
- "@jest/types" "^29.4.2"
- babel-jest "^29.4.2"
+ "@jest/test-sequencer" "^29.5.0"
+ "@jest/types" "^29.5.0"
+ babel-jest "^29.5.0"
chalk "^4.0.0"
ci-info "^3.2.0"
deepmerge "^4.2.2"
glob "^7.1.3"
graceful-fs "^4.2.9"
- jest-circus "^29.4.2"
- jest-environment-node "^29.4.2"
- jest-get-type "^29.4.2"
- jest-regex-util "^29.4.2"
- jest-resolve "^29.4.2"
- jest-runner "^29.4.2"
- jest-util "^29.4.2"
- jest-validate "^29.4.2"
+ jest-circus "^29.5.0"
+ jest-environment-node "^29.5.0"
+ jest-get-type "^29.4.3"
+ jest-regex-util "^29.4.3"
+ jest-resolve "^29.5.0"
+ jest-runner "^29.5.0"
+ jest-util "^29.5.0"
+ jest-validate "^29.5.0"
micromatch "^4.0.4"
parse-json "^5.2.0"
- pretty-format "^29.4.2"
+ pretty-format "^29.5.0"
slash "^3.0.0"
strip-json-comments "^3.1.1"
-jest-diff@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.2.tgz#b88502d5dc02d97f6512d73c37da8b36f49b4871"
- integrity sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g==
+jest-diff@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.5.0.tgz#e0d83a58eb5451dcc1fa61b1c3ee4e8f5a290d63"
+ integrity sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==
dependencies:
chalk "^4.0.0"
- diff-sequences "^29.4.2"
- jest-get-type "^29.4.2"
- pretty-format "^29.4.2"
+ diff-sequences "^29.4.3"
+ jest-get-type "^29.4.3"
+ pretty-format "^29.5.0"
-jest-docblock@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.2.tgz#c78a95eedf9a24c0a6cc16cf2abdc4b8b0f2531b"
- integrity sha512-dV2JdahgClL34Y5vLrAHde3nF3yo2jKRH+GIYJuCpfqwEJZcikzeafVTGAjbOfKPG17ez9iWXwUYp7yefeCRag==
+jest-docblock@^29.4.3:
+ version "29.4.3"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8"
+ integrity sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==
dependencies:
detect-newline "^3.0.0"
-jest-each@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.4.2.tgz#e1347aff1303f4c35470827a62c029d389c5d44a"
- integrity sha512-trvKZb0JYiCndc55V1Yh0Luqi7AsAdDWpV+mKT/5vkpnnFQfuQACV72IoRV161aAr6kAVIBpmYzwhBzm34vQkA==
+jest-each@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.5.0.tgz#fc6e7014f83eac68e22b7195598de8554c2e5c06"
+ integrity sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==
dependencies:
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
chalk "^4.0.0"
- jest-get-type "^29.4.2"
- jest-util "^29.4.2"
- pretty-format "^29.4.2"
-
-jest-environment-node@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.4.2.tgz#0eab835b41e25fd0c1a72f62665fc8db08762ad2"
- integrity sha512-MLPrqUcOnNBc8zTOfqBbxtoa8/Ee8tZ7UFW7hRDQSUT+NGsvS96wlbHGTf+EFAT9KC3VNb7fWEM6oyvmxtE/9w==
- dependencies:
- "@jest/environment" "^29.4.2"
- "@jest/fake-timers" "^29.4.2"
- "@jest/types" "^29.4.2"
+ jest-get-type "^29.4.3"
+ jest-util "^29.5.0"
+ pretty-format "^29.5.0"
+
+jest-environment-node@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.5.0.tgz#f17219d0f0cc0e68e0727c58b792c040e332c967"
+ integrity sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==
+ dependencies:
+ "@jest/environment" "^29.5.0"
+ "@jest/fake-timers" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
- jest-mock "^29.4.2"
- jest-util "^29.4.2"
+ jest-mock "^29.5.0"
+ jest-util "^29.5.0"
-jest-get-type@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.2.tgz#7cb63f154bca8d8f57364d01614477d466fa43fe"
- integrity sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg==
+jest-get-type@^29.4.3:
+ version "29.4.3"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.3.tgz#1ab7a5207c995161100b5187159ca82dd48b3dd5"
+ integrity sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==
-jest-haste-map@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.4.2.tgz#9112df3f5121e643f1b2dcbaa86ab11b0b90b49a"
- integrity sha512-WkUgo26LN5UHPknkezrBzr7lUtV1OpGsp+NfXbBwHztsFruS3gz+AMTTBcEklvi8uPzpISzYjdKXYZQJXBnfvw==
+jest-haste-map@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.5.0.tgz#69bd67dc9012d6e2723f20a945099e972b2e94de"
+ integrity sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==
dependencies:
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
"@types/graceful-fs" "^4.1.3"
"@types/node" "*"
anymatch "^3.0.3"
fb-watchman "^2.0.0"
graceful-fs "^4.2.9"
- jest-regex-util "^29.4.2"
- jest-util "^29.4.2"
- jest-worker "^29.4.2"
+ jest-regex-util "^29.4.3"
+ jest-util "^29.5.0"
+ jest-worker "^29.5.0"
micromatch "^4.0.4"
walker "^1.0.8"
optionalDependencies:
fsevents "^2.3.2"
-jest-leak-detector@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.4.2.tgz#8f05c6680e0cb46a1d577c0d3da9793bed3ea97b"
- integrity sha512-Wa62HuRJmWXtX9F00nUpWlrbaH5axeYCdyRsOs/+Rb1Vb6+qWTlB5rKwCCRKtorM7owNwKsyJ8NRDUcZ8ghYUA==
+jest-leak-detector@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.5.0.tgz#cf4bdea9615c72bac4a3a7ba7e7930f9c0610c8c"
+ integrity sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==
dependencies:
- jest-get-type "^29.4.2"
- pretty-format "^29.4.2"
+ jest-get-type "^29.4.3"
+ pretty-format "^29.5.0"
-jest-matcher-utils@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.2.tgz#08d0bf5abf242e3834bec92c7ef5071732839e85"
- integrity sha512-EZaAQy2je6Uqkrm6frnxBIdaWtSYFoR8SVb2sNLAtldswlR/29JAgx+hy67llT3+hXBaLB0zAm5UfeqerioZyg==
+jest-matcher-utils@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz#d957af7f8c0692c5453666705621ad4abc2c59c5"
+ integrity sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==
dependencies:
chalk "^4.0.0"
- jest-diff "^29.4.2"
- jest-get-type "^29.4.2"
- pretty-format "^29.4.2"
+ jest-diff "^29.5.0"
+ jest-get-type "^29.4.3"
+ pretty-format "^29.5.0"
-jest-message-util@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.2.tgz#309a2924eae6ca67cf7f25781a2af1902deee717"
- integrity sha512-SElcuN4s6PNKpOEtTInjOAA8QvItu0iugkXqhYyguRvQoXapg5gN+9RQxLAkakChZA7Y26j6yUCsFWN+hlKD6g==
+jest-message-util@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.5.0.tgz#1f776cac3aca332ab8dd2e3b41625435085c900e"
+ integrity sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==
dependencies:
"@babel/code-frame" "^7.12.13"
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
"@types/stack-utils" "^2.0.0"
chalk "^4.0.0"
graceful-fs "^4.2.9"
micromatch "^4.0.4"
- pretty-format "^29.4.2"
+ pretty-format "^29.5.0"
slash "^3.0.0"
stack-utils "^2.0.3"
-jest-mock@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.4.2.tgz#e1054be66fb3e975d26d4528fcde6979e4759de8"
- integrity sha512-x1FSd4Gvx2yIahdaIKoBjwji6XpboDunSJ95RpntGrYulI1ByuYQCKN/P7hvk09JB74IonU3IPLdkutEWYt++g==
+jest-mock@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.5.0.tgz#26e2172bcc71d8b0195081ff1f146ac7e1518aed"
+ integrity sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==
dependencies:
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
- jest-util "^29.4.2"
+ jest-util "^29.5.0"
jest-pnp-resolver@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e"
integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==
-jest-regex-util@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.2.tgz#19187cca35d301f8126cf7a021dd4dcb7b58a1ca"
- integrity sha512-XYZXOqUl1y31H6VLMrrUL1ZhXuiymLKPz0BO1kEeR5xER9Tv86RZrjTm74g5l9bPJQXA/hyLdaVPN/sdqfteig==
+jest-regex-util@^29.4.3:
+ version "29.4.3"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.3.tgz#a42616141e0cae052cfa32c169945d00c0aa0bb8"
+ integrity sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==
-jest-resolve-dependencies@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.2.tgz#6359db606f5967b68ca8bbe9dbc07a4306c12bf7"
- integrity sha512-6pL4ptFw62rjdrPk7rRpzJYgcRqRZNsZTF1VxVTZMishbO6ObyWvX57yHOaNGgKoADtAHRFYdHQUEvYMJATbDg==
+jest-resolve-dependencies@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.5.0.tgz#f0ea29955996f49788bf70996052aa98e7befee4"
+ integrity sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==
dependencies:
- jest-regex-util "^29.4.2"
- jest-snapshot "^29.4.2"
+ jest-regex-util "^29.4.3"
+ jest-snapshot "^29.5.0"
-jest-resolve@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.4.2.tgz#8831f449671d08d161fe493003f61dc9b55b808e"
- integrity sha512-RtKWW0mbR3I4UdkOrW7552IFGLYQ5AF9YrzD0FnIOkDu0rAMlA5/Y1+r7lhCAP4nXSBTaE7ueeqj6IOwZpgoqw==
+jest-resolve@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.5.0.tgz#b053cc95ad1d5f6327f0ac8aae9f98795475ecdc"
+ integrity sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==
dependencies:
chalk "^4.0.0"
graceful-fs "^4.2.9"
- jest-haste-map "^29.4.2"
+ jest-haste-map "^29.5.0"
jest-pnp-resolver "^1.2.2"
- jest-util "^29.4.2"
- jest-validate "^29.4.2"
+ jest-util "^29.5.0"
+ jest-validate "^29.5.0"
resolve "^1.20.0"
resolve.exports "^2.0.0"
slash "^3.0.0"
-jest-runner@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.4.2.tgz#2bcecf72303369df4ef1e6e983c22a89870d5125"
- integrity sha512-wqwt0drm7JGjwdH+x1XgAl+TFPH7poowMguPQINYxaukCqlczAcNLJiK+OLxUxQAEWMdy+e6nHZlFHO5s7EuRg==
+jest-runner@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.5.0.tgz#6a57c282eb0ef749778d444c1d758c6a7693b6f8"
+ integrity sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==
dependencies:
- "@jest/console" "^29.4.2"
- "@jest/environment" "^29.4.2"
- "@jest/test-result" "^29.4.2"
- "@jest/transform" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/console" "^29.5.0"
+ "@jest/environment" "^29.5.0"
+ "@jest/test-result" "^29.5.0"
+ "@jest/transform" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
chalk "^4.0.0"
emittery "^0.13.1"
graceful-fs "^4.2.9"
- jest-docblock "^29.4.2"
- jest-environment-node "^29.4.2"
- jest-haste-map "^29.4.2"
- jest-leak-detector "^29.4.2"
- jest-message-util "^29.4.2"
- jest-resolve "^29.4.2"
- jest-runtime "^29.4.2"
- jest-util "^29.4.2"
- jest-watcher "^29.4.2"
- jest-worker "^29.4.2"
+ jest-docblock "^29.4.3"
+ jest-environment-node "^29.5.0"
+ jest-haste-map "^29.5.0"
+ jest-leak-detector "^29.5.0"
+ jest-message-util "^29.5.0"
+ jest-resolve "^29.5.0"
+ jest-runtime "^29.5.0"
+ jest-util "^29.5.0"
+ jest-watcher "^29.5.0"
+ jest-worker "^29.5.0"
p-limit "^3.1.0"
source-map-support "0.5.13"
-jest-runtime@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.4.2.tgz#d86b764c5b95d76cb26ed1f32644e99de5d5c134"
- integrity sha512-3fque9vtpLzGuxT9eZqhxi+9EylKK/ESfhClv4P7Y9sqJPs58LjVhTt8jaMp/pRO38agll1CkSu9z9ieTQeRrw==
- dependencies:
- "@jest/environment" "^29.4.2"
- "@jest/fake-timers" "^29.4.2"
- "@jest/globals" "^29.4.2"
- "@jest/source-map" "^29.4.2"
- "@jest/test-result" "^29.4.2"
- "@jest/transform" "^29.4.2"
- "@jest/types" "^29.4.2"
+jest-runtime@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.5.0.tgz#c83f943ee0c1da7eb91fa181b0811ebd59b03420"
+ integrity sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==
+ dependencies:
+ "@jest/environment" "^29.5.0"
+ "@jest/fake-timers" "^29.5.0"
+ "@jest/globals" "^29.5.0"
+ "@jest/source-map" "^29.4.3"
+ "@jest/test-result" "^29.5.0"
+ "@jest/transform" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
chalk "^4.0.0"
cjs-module-lexer "^1.0.0"
collect-v8-coverage "^1.0.0"
glob "^7.1.3"
graceful-fs "^4.2.9"
- jest-haste-map "^29.4.2"
- jest-message-util "^29.4.2"
- jest-mock "^29.4.2"
- jest-regex-util "^29.4.2"
- jest-resolve "^29.4.2"
- jest-snapshot "^29.4.2"
- jest-util "^29.4.2"
- semver "^7.3.5"
+ jest-haste-map "^29.5.0"
+ jest-message-util "^29.5.0"
+ jest-mock "^29.5.0"
+ jest-regex-util "^29.4.3"
+ jest-resolve "^29.5.0"
+ jest-snapshot "^29.5.0"
+ jest-util "^29.5.0"
slash "^3.0.0"
strip-bom "^4.0.0"
-jest-snapshot@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.4.2.tgz#ba1fb9abb279fd2c85109ff1757bc56b503bbb3a"
- integrity sha512-PdfubrSNN5KwroyMH158R23tWcAXJyx4pvSvWls1dHoLCaUhGul9rsL3uVjtqzRpkxlkMavQjGuWG1newPgmkw==
+jest-snapshot@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.5.0.tgz#c9c1ce0331e5b63cd444e2f95a55a73b84b1e8ce"
+ integrity sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==
dependencies:
"@babel/core" "^7.11.6"
"@babel/generator" "^7.7.2"
@@ -2648,87 +2705,81 @@ jest-snapshot@^29.4.2:
"@babel/plugin-syntax-typescript" "^7.7.2"
"@babel/traverse" "^7.7.2"
"@babel/types" "^7.3.3"
- "@jest/expect-utils" "^29.4.2"
- "@jest/transform" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/expect-utils" "^29.5.0"
+ "@jest/transform" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/babel__traverse" "^7.0.6"
"@types/prettier" "^2.1.5"
babel-preset-current-node-syntax "^1.0.0"
chalk "^4.0.0"
- expect "^29.4.2"
+ expect "^29.5.0"
graceful-fs "^4.2.9"
- jest-diff "^29.4.2"
- jest-get-type "^29.4.2"
- jest-haste-map "^29.4.2"
- jest-matcher-utils "^29.4.2"
- jest-message-util "^29.4.2"
- jest-util "^29.4.2"
+ jest-diff "^29.5.0"
+ jest-get-type "^29.4.3"
+ jest-matcher-utils "^29.5.0"
+ jest-message-util "^29.5.0"
+ jest-util "^29.5.0"
natural-compare "^1.4.0"
- pretty-format "^29.4.2"
+ pretty-format "^29.5.0"
semver "^7.3.5"
-jest-util@^29.0.0, jest-util@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.2.tgz#3db8580b295df453a97de4a1b42dd2578dabd2c2"
- integrity sha512-wKnm6XpJgzMUSRFB7YF48CuwdzuDIHenVuoIb1PLuJ6F+uErZsuDkU+EiExkChf6473XcawBrSfDSnXl+/YG4g==
+jest-util@^29.0.0, jest-util@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f"
+ integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==
dependencies:
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
chalk "^4.0.0"
ci-info "^3.2.0"
graceful-fs "^4.2.9"
picomatch "^2.2.3"
-jest-validate@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.4.2.tgz#3b3f8c4910ab9a3442d2512e2175df6b3f77b915"
- integrity sha512-tto7YKGPJyFbhcKhIDFq8B5od+eVWD/ySZ9Tvcp/NGCvYA4RQbuzhbwYWtIjMT5W5zA2W0eBJwu4HVw34d5G6Q==
+jest-validate@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.5.0.tgz#8e5a8f36178d40e47138dc00866a5f3bd9916ffc"
+ integrity sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==
dependencies:
- "@jest/types" "^29.4.2"
+ "@jest/types" "^29.5.0"
camelcase "^6.2.0"
chalk "^4.0.0"
- jest-get-type "^29.4.2"
+ jest-get-type "^29.4.3"
leven "^3.1.0"
- pretty-format "^29.4.2"
+ pretty-format "^29.5.0"
-jest-watcher@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.4.2.tgz#09c0f4c9a9c7c0807fcefb1445b821c6f7953b7c"
- integrity sha512-onddLujSoGiMJt+tKutehIidABa175i/Ays+QvKxCqBwp7fvxP3ZhKsrIdOodt71dKxqk4sc0LN41mWLGIK44w==
+jest-watcher@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.5.0.tgz#cf7f0f949828ba65ddbbb45c743a382a4d911363"
+ integrity sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==
dependencies:
- "@jest/test-result" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/test-result" "^29.5.0"
+ "@jest/types" "^29.5.0"
"@types/node" "*"
ansi-escapes "^4.2.1"
chalk "^4.0.0"
emittery "^0.13.1"
- jest-util "^29.4.2"
+ jest-util "^29.5.0"
string-length "^4.0.1"
-jest-worker@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.2.tgz#d9b2c3bafc69311d84d94e7fb45677fc8976296f"
- integrity sha512-VIuZA2hZmFyRbchsUCHEehoSf2HEl0YVF8SDJqtPnKorAaBuh42V8QsLnde0XP5F6TyCynGPEGgBOn3Fc+wZGw==
+jest-worker@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.5.0.tgz#bdaefb06811bd3384d93f009755014d8acb4615d"
+ integrity sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==
dependencies:
"@types/node" "*"
- jest-util "^29.4.2"
+ jest-util "^29.5.0"
merge-stream "^2.0.0"
supports-color "^8.0.0"
-jest@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/jest/-/jest-29.4.2.tgz#4c2127d03a71dc187f386156ef155dbf323fb7be"
- integrity sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg==
+jest@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-29.5.0.tgz#f75157622f5ce7ad53028f2f8888ab53e1f1f24e"
+ integrity sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==
dependencies:
- "@jest/core" "^29.4.2"
- "@jest/types" "^29.4.2"
+ "@jest/core" "^29.5.0"
+ "@jest/types" "^29.5.0"
import-local "^3.0.2"
- jest-cli "^29.4.2"
-
-js-sdsl@^4.1.4:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711"
- integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==
+ jest-cli "^29.5.0"
js-tokens@^4.0.0:
version "4.0.0"
@@ -2852,6 +2903,11 @@ lodash@^4.14.2, lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+long@^5.2.1:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
+ integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==
+
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -2866,15 +2922,25 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+lru-cache@^7.14.1:
+ version "7.18.3"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
+ integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
+
+lru-cache@^8.0.0:
+ version "8.0.5"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-8.0.5.tgz#983fe337f3e176667f8e567cfcce7cb064ea214e"
+ integrity sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==
+
lru-cache@~2.2.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
integrity sha512-Q5pAgXs+WEAfoEdw2qKQhNFFhMoFMTYqRVKKUMnzuiR7oKFHS7fWo848cPcTKw+4j/IdN17NyzdhVKgabFV0EA==
-luxon@^3.2.1:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.2.1.tgz#14f1af209188ad61212578ea7e3d518d18cee45f"
- integrity sha512-QrwPArQCNLAKGO/C+ZIilgIuDnEnKx5QYODdDtbFaxzsbZcc/a7WFq7MhsVYgRlwawLtvOUESTlfJ+hc/USqPg==
+luxon@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48"
+ integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==
make-dir@^3.0.0:
version "3.1.0"
@@ -2986,15 +3052,26 @@ ms@2.1.3, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-mysql@^2.18.1:
- version "2.18.1"
- resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717"
- integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==
+mysql2@^3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.3.1.tgz#b7e055812adeb46d96bc65a5b37fa57e70bc0388"
+ integrity sha512-UD84/AvLwO5qmSABEsBTZ7y7JKv3sM8JzWGhuL4tDkJwVsClVVAcelNSR5Unyhxj6/KHBAkjS7qe5/c+gEmNvA==
+ dependencies:
+ denque "^2.1.0"
+ generate-function "^2.3.1"
+ iconv-lite "^0.6.3"
+ long "^5.2.1"
+ lru-cache "^8.0.0"
+ named-placeholders "^1.1.3"
+ seq-queue "^0.0.5"
+ sqlstring "^2.3.2"
+
+named-placeholders@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/named-placeholders/-/named-placeholders-1.1.3.tgz#df595799a36654da55dda6152ba7a137ad1d9351"
+ integrity sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==
dependencies:
- bignumber.js "9.0.0"
- readable-stream "2.3.7"
- safe-buffer "5.1.2"
- sqlstring "2.3.1"
+ lru-cache "^7.14.1"
natural-compare-lite@^1.4.0:
version "1.4.0"
@@ -3017,14 +3094,14 @@ node-int64@^0.4.0:
integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==
node-releases@^2.0.8:
- version "2.0.10"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
- integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
+ version "2.0.11"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.11.tgz#59d7cef999d13f908e43b5a70001cf3129542f0f"
+ integrity sha512-+M0PwXeU80kRohZ3aT4J/OnR+l9/KD2nVLNNoRgFtnf+umQVFdGBAO2N8+nCnEi0xlh/Wk3zOGC+vNNx+uM79Q==
-nodemon@^2.0.15:
- version "2.0.20"
- resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.20.tgz#e3537de768a492e8d74da5c5813cb0c7486fc701"
- integrity sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==
+nodemon@^2.0.22:
+ version "2.0.22"
+ resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258"
+ integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==
dependencies:
chokidar "^3.5.2"
debug "^3.2.7"
@@ -3221,25 +3298,20 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
-prettier@^2.8.4:
- version "2.8.4"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.4.tgz#34dd2595629bfbb79d344ac4a91ff948694463c3"
- integrity sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==
+prettier@^2.8.8:
+ version "2.8.8"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
+ integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
-pretty-format@^29.0.0, pretty-format@^29.4.2:
- version "29.4.2"
- resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.2.tgz#64bf5ccc0d718c03027d94ac957bdd32b3fb2401"
- integrity sha512-qKlHR8yFVCbcEWba0H0TOC8dnLlO4vPlyEjRPw31FZ2Rupy9nLa8ZLbYny8gWEl8CkEhJqAE6IzdNELTBVcBEg==
+pretty-format@^29.0.0, pretty-format@^29.5.0:
+ version "29.5.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a"
+ integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==
dependencies:
- "@jest/schemas" "^29.4.2"
+ "@jest/schemas" "^29.4.3"
ansi-styles "^5.0.0"
react-is "^18.0.0"
-process-nextick-args@~2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
- integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-
prompts@^2.0.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
@@ -3266,13 +3338,25 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"
integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
-qs@6.11.0, qs@^6.11.0:
+pure-rand@^6.0.0:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306"
+ integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==
+
+qs@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
dependencies:
side-channel "^1.0.4"
+qs@^6.11.0:
+ version "6.11.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
+ integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==
+ dependencies:
+ side-channel "^1.0.4"
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -3303,19 +3387,6 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
-readable-stream@2.3.7:
- version "2.3.7"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
- integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.3"
- isarray "~1.0.0"
- process-nextick-args "~2.0.0"
- safe-buffer "~5.1.1"
- string_decoder "~1.1.1"
- util-deprecate "~1.0.1"
-
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -3330,37 +3401,17 @@ rechoir@^0.8.0:
dependencies:
resolve "^1.20.0"
-redis-commands@^1.7.0:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
- integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==
-
-redis-errors@^1.0.0, redis-errors@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
- integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==
-
-redis-parser@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4"
- integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==
+redis@^4.6.6:
+ version "4.6.6"
+ resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.6.tgz#46d4f2d149d1634d6ef53db5747412a0ef7974ec"
+ integrity sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==
dependencies:
- redis-errors "^1.0.0"
-
-redis@^3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.2.tgz#766851117e80653d23e0ed536254677ab647638c"
- integrity sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==
- dependencies:
- denque "^1.5.0"
- redis-commands "^1.7.0"
- redis-errors "^1.2.0"
- redis-parser "^3.0.0"
-
-regexpp@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
- integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
+ "@redis/bloom" "1.2.0"
+ "@redis/client" "1.5.7"
+ "@redis/graph" "1.1.0"
+ "@redis/json" "1.0.4"
+ "@redis/search" "1.1.2"
+ "@redis/time-series" "1.0.4"
request-ip@~2.0.1:
version "2.0.2"
@@ -3392,16 +3443,16 @@ resolve-from@^5.0.0:
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
resolve.exports@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e"
- integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg==
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800"
+ integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==
resolve@^1.20.0:
- version "1.22.1"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
- integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
+ version "1.22.2"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f"
+ integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
dependencies:
- is-core-module "^2.9.0"
+ is-core-module "^2.11.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
@@ -3439,25 +3490,20 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
-safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
- integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
safe-buffer@5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-"safer-buffer@>= 2.1.2 < 3":
+"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
semver@7.x, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8:
- version "7.3.8"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
- integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
+ version "7.5.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec"
+ integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==
dependencies:
lru-cache "^6.0.0"
@@ -3495,6 +3541,11 @@ send@0.18.0:
range-parser "~1.2.1"
statuses "2.0.1"
+seq-queue@^0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e"
+ integrity sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==
+
serve-static@1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
@@ -3561,22 +3612,22 @@ socket.io-adapter@~2.5.2:
ws "~8.11.0"
socket.io-parser@~4.2.1:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206"
- integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667"
+ integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
-socket.io@^4.6.0:
- version "4.6.0"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.0.tgz#82ebfd7652572872e10dbb19533fc7cb930d0bc3"
- integrity sha512-b65bp6INPk/BMMrIgVvX12x3Q+NqlGqSlTuvKQWt0BUJ3Hyy3JangBl7fEoWZTXbOKlCqNPbQ6MbWgok/km28w==
+socket.io@^4.6.1:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70"
+ integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==
dependencies:
accepts "~1.3.4"
base64id "~2.0.0"
debug "~4.3.2"
- engine.io "~6.4.0"
+ engine.io "~6.4.1"
socket.io-adapter "~2.5.2"
socket.io-parser "~4.2.1"
@@ -3603,10 +3654,10 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
-sqlstring@2.3.1:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40"
- integrity sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==
+sqlstring@^2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c"
+ integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==
stack-utils@^2.0.3:
version "2.0.6"
@@ -3642,13 +3693,6 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
-string_decoder@~1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
- integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
- dependencies:
- safe-buffer "~5.1.0"
-
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
@@ -3779,10 +3823,10 @@ traverse-chain@~0.1.0:
resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1"
integrity sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==
-ts-jest@^29.0.5:
- version "29.0.5"
- resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.5.tgz#c5557dcec8fe434fcb8b70c3e21c6b143bfce066"
- integrity sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==
+ts-jest@^29.1.0:
+ version "29.1.0"
+ resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.0.tgz#4a9db4104a49b76d2b368ea775b6c9535c603891"
+ integrity sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==
dependencies:
bs-logger "0.x"
fast-json-stable-stringify "2.x"
@@ -3854,10 +3898,10 @@ type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
-typescript@^4.9.5:
- version "4.9.5"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
- integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
+typescript@^5.0.4:
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
+ integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
uid-safe@~2.1.5:
version "2.1.5"
@@ -3877,9 +3921,9 @@ unpipe@1.0.0, unpipe@~1.0.0:
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
update-browserslist-db@^1.0.10:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
- integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
+ integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
@@ -3891,11 +3935,6 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
-util-deprecate@~1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
- integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
@@ -3907,9 +3946,9 @@ v8-compile-cache-lib@^3.0.1:
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
v8-to-istanbul@^9.0.1:
- version "9.0.1"
- resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4"
- integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265"
+ integrity sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==
dependencies:
"@jridgewell/trace-mapping" "^0.3.12"
"@types/istanbul-lib-coverage" "^2.0.1"
@@ -3971,25 +4010,25 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+yallist@4.0.0, yallist@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+ integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
-yallist@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
- integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
yargs-parser@^21.0.1, yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@^17.3.1:
- version "17.6.2"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541"
- integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==
+ version "17.7.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
+ integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"
diff --git a/db/migration/V021__create_assessments.sql b/db/migration/V021__create_assessments.sql
new file mode 100644
index 00000000..9183db29
--- /dev/null
+++ b/db/migration/V021__create_assessments.sql
@@ -0,0 +1,155 @@
+CREATE TABLE assessment_question_types (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ title TEXT NOT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE assessment_submission_states (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ title TEXT NOT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE program_participant_roles (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ title TEXT NOT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE program_participants (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ principal_id BIGINT NOT NULL,
+ program_id BIGINT NOT NULL,
+ role_id BIGINT,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX program_participants_principal_id (principal_id),
+ FOREIGN KEY (principal_id) REFERENCES principals(id),
+ INDEX program_participants_program_id (program_id),
+ FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE,
+ INDEX program_participants_role_id (role_id),
+ FOREIGN KEY (role_id) REFERENCES program_participant_roles(id) ON DELETE SET NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY `principals_and_programs` (`principal_id`,`program_id`)
+);
+
+CREATE TABLE curriculum_assessments (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ title TEXT NOT NULL,
+ description TEXT NOT NULL,
+ max_score INT,
+ max_num_submissions INT NOT NULL DEFAULT 1,
+ time_limit INT,
+ curriculum_id BIGINT NOT NULL,
+ activity_id BIGINT NULL,
+ principal_id BIGINT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX curriculum_assessments_curriculum_id (curriculum_id),
+ FOREIGN KEY (curriculum_id) REFERENCES curriculums(id) ON DELETE CASCADE,
+ INDEX curriculum_assessments_activity_id (activity_id),
+ FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE SET NULL,
+ INDEX curriculum_assessments_principal_id (principal_id),
+ FOREIGN KEY (principal_id) REFERENCES principals(id),
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE program_assessments (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ program_id BIGINT NOT NULL,
+ assessment_id BIGINT NOT NULL,
+ available_after TIMESTAMP NOT NULL,
+ due_date TIMESTAMP NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX program_assessments_program_id (program_id),
+ FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE,
+ INDEX program_assessments_assessment_id (assessment_id),
+ FOREIGN KEY (assessment_id) REFERENCES curriculum_assessments(id) ON DELETE CASCADE,
+ PRIMARY KEY (id),
+ UNIQUE KEY `program_and_assessment` (`program_id`,`assessment_id`)
+);
+
+CREATE TABLE assessment_questions (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ assessment_id BIGINT NOT NULL,
+ title TEXT NOT NULL,
+ description TEXT,
+ question_type_id BIGINT NOT NULL,
+ correct_answer_id BIGINT,
+ max_score INT,
+ sort_order INT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX assessment_answers_assessment_question_id (assessment_id),
+ FOREIGN KEY (assessment_id) REFERENCES curriculum_assessments(id) ON DELETE CASCADE,
+ INDEX assessment_questions_question_type_id (question_type_id),
+ FOREIGN KEY (question_type_id) REFERENCES assessment_question_types(id),
+ PRIMARY KEY (id),
+ UNIQUE KEY `assessment_and_question_number` (`assessment_id`,`sort_order`)
+);
+
+CREATE TABLE assessment_answers (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ question_id BIGINT,
+ title TEXT NOT NULL,
+ description TEXT,
+ sort_order INT NOT NULL,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX assessment_answers_question_id (question_id),
+ FOREIGN KEY (question_id) REFERENCES assessment_questions(id) ON DELETE CASCADE,
+ PRIMARY KEY (id),
+ UNIQUE KEY `question_and_answer_order` (`question_id`,`sort_order`)
+);
+
+CREATE TABLE assessment_submissions (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ assessment_id BIGINT NOT NULL,
+ principal_id BIGINT NOT NULL,
+ assessment_submission_state_id BIGINT NOT NULL,
+ score INT,
+ opened_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ submitted_at TIMESTAMP,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX assessment_submissions_assessment_id (assessment_id),
+ FOREIGN KEY (assessment_id) REFERENCES program_assessments(id),
+ INDEX assessment_submissions_principal_id (principal_id),
+ FOREIGN KEY (principal_id) REFERENCES principals(id),
+ INDEX assessment_submissions_assessment_submission_state_id (assessment_submission_state_id),
+ FOREIGN KEY (assessment_submission_state_id) REFERENCES assessment_submission_states(id),
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE assessment_responses (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ assessment_id BIGINT NOT NULL,
+ submission_id BIGINT NOT NULL,
+ question_id BIGINT NOT NULL,
+ answer_id BIGINT,
+ response TEXT,
+ score INT,
+ grader_response TEXT,
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ INDEX assessment_responses_assessment_id (assessment_id),
+ FOREIGN KEY (assessment_id) REFERENCES program_assessments(id),
+ INDEX assessment_responses_submission_id (submission_id),
+ FOREIGN KEY (submission_id) REFERENCES assessment_submissions(id) ON DELETE CASCADE,
+ INDEX assessment_responses_question_id (question_id),
+ FOREIGN KEY (question_id) REFERENCES assessment_questions(id),
+ INDEX assessment_responses_answer_id (answer_id),
+ FOREIGN KEY (answer_id) REFERENCES assessment_answers(id) ON DELETE SET NULL,
+ PRIMARY KEY (id),
+ UNIQUE KEY `submission_and_question` (`submission_id`,`question_id`)
+);
+
+ALTER TABLE questionnaires
+RENAME TO surveys;
+
+ALTER TABLE prompts
+RENAME TO survey_questions;
+
+ALTER TABLE options
+RENAME TO survey_answers;
diff --git a/db/migration/V022__insert_sample_assessment_data.sql b/db/migration/V022__insert_sample_assessment_data.sql
new file mode 100644
index 00000000..834cc5a5
--- /dev/null
+++ b/db/migration/V022__insert_sample_assessment_data.sql
@@ -0,0 +1,163 @@
+INSERT INTO `activity_types`
+ (`title`)
+VALUES
+ ("quiz"),
+ ("test");
+
+INSERT INTO `assessment_submission_states`
+ (`title`)
+VALUES
+ ("Inactive"),
+ ("Active"),
+ ("Opened"),
+ ("In Progress"),
+ ("Expired"),
+ ("Submitted"),
+ ("Graded"),
+ ("Hidden");
+
+INSERT INTO `program_participant_roles`
+ (`title`)
+VALUES
+ ("Facilitator"),
+ ("Participant");
+
+INSERT INTO `curriculums`
+ (`title`, `principal_id`)
+VALUES
+ ("Beginner Web Development", 2);
+
+INSERT INTO `programs`
+ (`title`, `start_date`, `end_date`, `time_zone`, `principal_id`, `curriculum_id`)
+VALUES
+ ("Cohort 5", "2023-02-06", "2023-03-31", "America/Vancouver", 2, 3);
+
+INSERT INTO `activities`
+ (`title`, `description_text`, `curriculum_week`, `curriculum_day`, `start_time`, `end_time`, `duration`, `activity_type_id`, `curriculum_id`)
+VALUES
+ ("Assignment 1: React", "Your assignment for week 1 learning.", 1, 5, NULL, NULL, NULL, 1, 3),
+ ("SQL Quiz", "A check on your SQL learning.", 7, 5, NULL, NULL, NULL, 10, 3),
+ ("Final Exam", "The final exam for the course.", 8, 5, NULL, NULL, NULL, 11, 3);
+
+INSERT INTO `assessment_question_types`
+ (`title`)
+VALUES
+ ("single choice"),
+ ("free response");
+
+INSERT INTO `curriculum_assessments`
+ (`title`, `description`, `max_score`, `max_num_submissions`, `time_limit`, `curriculum_id`, `activity_id`, `principal_id`)
+VALUES
+ ("Assignment 1: React", "Your assignment for week 1 learning.", 10, 1, NULL, 3, 97, 2),
+ ("SQL Quiz", "A check on your SQL learning.", 5, 3, NULL, 3, 98, 2),
+ ("Final Exam", "The final exam for the course.", 50, 1, 120, 3, 99, 2);
+
+INSERT INTO `program_assessments`
+ (`program_id`, `assessment_id`, `available_after`, `due_date`)
+VALUES
+ (2, 1, "2033-02-06", "2033-06-10"),
+ (2, 2, "2023-02-06", "2033-06-24"),
+ (2, 3, "2023-02-20", "2033-06-30");
+
+INSERT INTO `assessment_questions`
+ (`assessment_id`, `title`, `description`, `question_type_id`, `correct_answer_id`, `max_score`, `sort_order`)
+VALUES
+ (1, "What is React?", NULL, 1, 4, 1, 1),
+ (1, "What is the purpose of JSX in React?", NULL, 1, 5, 1, 2),
+ (1, "What is the virtual DOM in React?", NULL, 1, 9, 1, 3),
+ (1, "What are React components?", NULL, 1, 14, 1, 4),
+ (1, "When lifting state up, you need to move the useState from a child component to a parent component.", NULL, 1, 18, 1, 5),
+ (1, "Which of the following options describe what a prop is in React", NULL, 1, 21, 1, 6),
+ (1, "If the state variable holds an array or a string value, once you pass that state via props from a parent to a child, you can then read the length property from the received prop in the child component", NULL, 1, 23, 1, 7),
+ (1, "Write a React JSX component that displays the text “Hello, World!” on the screen.", NULL, 2, NULL, 1, 8),
+ (1, "How does React differ from other JavaScript frameworks?", NULL, 2, NULL, 1, 9),
+ (1, "What are some of the benefits of using React?", NULL, 2, NULL, 1, 10),
+ (2, "A JOIN clause is used to combine rows from two or more tables, based on a related column between them. Which command is not a JOIN type in SQL?", NULL, 1, 30, 1, 1),
+ (2, "Which statement is wrong?", NULL, 1, 32, 2, 2),
+ (2, "How can we add a column to a table?", NULL, 1, 39, 1, 3),
+ (2, "Each table can contain more than one primary key.", NULL, 1, 41, 1, 4),
+ (3, "What is MySQL?", NULL, 1, 42, 1, 1),
+ (3, "INT and VARCHAR are some common data types in MySQL.", NULL, 1, 46, 1, 2),
+ (3, "Which command is used to create a new database in MySQL?", NULL, 1, 50, 1, 3),
+ (3, "COUNT, SUM, and INSERT INTO are some common MySQL aggregate functions.", NULL, 1, 53, 1, 4),
+ (3, "Which command is used to insert new data into a MySQL table?", NULL, 1, 57, 1, 5),
+ (3, "Which command is used to delete a table from a MySQL database?", NULL, 1, 59, 1, 6),
+ (3, "Which builtin MySQL function can be used to add every value from a column together in a query?", NULL, 1, 65, 1, 7),
+ (3, "Which command is used to retrieve data from a MySQL table?", NULL, 2, NULL, 9, 8),
+ (3, "Which function is used to count the number of rows in a MySQL table?", NULL, 2, NULL, 9, 9),
+ (3, "Which keyword is used to specify the condition for a MySQL query?", NULL, 2, NULL, 10, 10);
+
+INSERT INTO `assessment_answers`
+ (`question_id`, `title`, `description`, `sort_order`)
+VALUES
+ (1, "A relational database management system", NULL, 1),
+ (1, "A database management system", NULL, 2),
+ (1, "A web server software", NULL, 3),
+ (1, "A front-end JavaScript library", NULL, 4),
+ (2, "To provide a syntax for writing HTML in JavaScript", NULL, 1),
+ (2, "To provide a syntax for writing JavaScript in HTML", NULL, 2),
+ (2, "To provide a syntax for writing SQL in JavaScript", NULL, 3),
+ (2, "To provide a syntax for writing CSS in JavaScript", NULL, 4),
+ (3, "A virtual representation of the actual HTML DOM", NULL, 1),
+ (3, "A physical representation of the actual HTML DOM", NULL, 2),
+ (3, "A database of all the components in a React application", NULL, 3),
+ (3, "A list of all the CSS styles used in a React application", NULL, 4),
+ (4, "An object-oriented programming language feature", NULL, 1),
+ (4, "Reusable pieces of UI", NULL, 2),
+ (4, "A way to define styles in React", NULL, 3),
+ (4, "A way to define database schemas in React", NULL, 4),
+ (5, "True", NULL, 1),
+ (5, "False", NULL, 2),
+ (6, "A built-in method for creating React components", NULL, 1),
+ (6, "A type of HTML tag used to define a custom element", NULL, 2),
+ (6, "A data object passed from a parent component to a child component", NULL, 3),
+ (6, "A tool that helps optimize the performance of a React app", NULL, 4),
+ (7, "True", NULL, 1),
+ (7, "False", NULL, 2),
+ (8, "const HelloWorld = () => { return Hello, World!
; }; export default HelloWorld;", NULL, 1),
+ (9, "React differs from other JavaScript frameworks because it uses a component-based architecture, a virtual DOM, JSX syntax, unidirectional data flow, and is primarily focused on building UIs rather than providing a complete application framework. These features make it faster, more efficient, and more flexible than other frameworks.", NULL, 1),
+ (10, "Benefits of using React include its modular and reusable components, efficient updates with virtual DOM, JSX syntax, and active community.", NULL, 1),
+ (11, "Self Join", NULL, 1),
+ (11, "Full Join", NULL, 2),
+ (11, "Half Join", NULL, 3),
+ (11, "Inner Join", NULL, 4),
+ (12, "TypeScript is known as a dynamically-typed language", NULL, 1),
+ (12, "JavaScript is both a functional as well as object-oriented programming language", NULL, 2),
+ (12, "TypeScript takes a long time to compile the code", NULL, 3),
+ (12, "During compile time, the source code is translated to a byte code", NULL, 4),
+ (13, "CREATE TABLE", NULL, 1),
+ (13, "ADD DATA", NULL, 2),
+ (13, "INSERT DATA", NULL, 3),
+ (13, "ALTER TABLE", NULL, 4),
+ (14, "True", NULL, 1),
+ (14, "False", NULL, 2),
+ (15, "A relational database management system", NULL, 1),
+ (15, "A programming language", NULL, 2),
+ (15, "An operating system", NULL, 3),
+ (15, "A web server", NULL, 4),
+ (16, "True", NULL, 1),
+ (16, "False", NULL, 2),
+ (17, "CREATE TABLE", NULL, 1),
+ (17, "CREATE INDEX", NULL, 2),
+ (17, "CREATE DATABASE", NULL, 3),
+ (17, "CREATE SCHEMA", NULL, 4),
+ (18, "True", NULL, 1),
+ (18, "False", NULL, 2),
+ (19, "ADD DATA", NULL, 1),
+ (19, "INSERT DATA", NULL, 2),
+ (19, "INSERT ROW", NULL, 3),
+ (19, "INSERT INTO", NULL, 4),
+ (20, "DELETE TABLE", NULL, 1),
+ (20, "DROP TABLE", NULL, 2),
+ (20, "REMOVE TABLE", NULL, 3),
+ (20, "ERASE TABLE", NULL, 4),
+ (21, "MAX", NULL, 1),
+ (21, "TOGETHER", NULL, 2),
+ (21, "TOTAL", NULL, 3),
+ (21, "SUM", NULL, 4),
+ (21, "MIN", NULL, 5),
+ (21, "SUMTOTAL", NULL, 6),
+ (21, "TOTALSUM", NULL, 7),
+ (22, "The SELECT command is used to retrieve data from a MySQL table.", NULL, 1),
+ (23, "COUNT()", NULL, 1),
+ (24, "WHERE", NULL, 1);
diff --git a/docker-compose.yml b/docker-compose.yml
index 86ee101c..1eea50e4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,7 +18,7 @@ services:
- SESSION_SECRET
- WEBAPP_ORIGIN
mysql:
- command: --default-authentication-plugin=mysql_native_password
+ command: --skip-name-resolve
environment:
- MYSQL_DATABASE
- MYSQL_PASSWORD
@@ -29,9 +29,10 @@ services:
- "3306:3306"
healthcheck:
test: mysql ${MYSQL_DATABASE} --user=${MYSQL_USER} --password='${MYSQL_PASSWORD}' --silent --execute "SELECT 1;"
- interval: 10s
- timeout: 10s
- retries: 12
+ interval: 3s
+ timeout: 3s
+ start_period: 60s
+ retries: 180
flyway:
build: ./db/
command: migrate
@@ -56,7 +57,7 @@ services:
ports:
- "80:80"
redis:
- image: redis:6.2-alpine
+ image: redis:7-alpine
ports:
- "6379:6379"
webapp:
diff --git a/nginx/Dockerfile b/nginx/Dockerfile
index e005648b..b446ba55 100644
--- a/nginx/Dockerfile
+++ b/nginx/Dockerfile
@@ -1,3 +1,3 @@
-FROM nginx:1.23-alpine
+FROM nginx:1.24-alpine
ARG BUILD_ENV
COPY ./${BUILD_ENV}/nginx.conf /etc/nginx/nginx.conf
diff --git a/webapp/.nvmrc b/webapp/.nvmrc
index ea438c37..d3e33db8 100644
--- a/webapp/.nvmrc
+++ b/webapp/.nvmrc
@@ -1 +1 @@
-v18.14
+v18.16
diff --git a/webapp/Dockerfile b/webapp/Dockerfile
index b1a9b920..de7a1128 100644
--- a/webapp/Dockerfile
+++ b/webapp/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:18.14-alpine AS build
+FROM node:18.16-alpine AS build
ARG REACT_APP_API_ORIGIN
ENV REACT_APP_API_ORIGIN $REACT_APP_API_ORIGIN
ARG REACT_APP_ROLLBAR_ACCESS_TOKEN
@@ -9,6 +9,6 @@ RUN yarn install --ignore-platform --frozen-lockfile --network-timeout 600000
COPY ./ ./
RUN yarn build
-FROM nginx:1.23-alpine
+FROM nginx:1.24-alpine
COPY ./default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/build/ /usr/share/nginx/html/
diff --git a/webapp/package.json b/webapp/package.json
index b85e148a..40621399 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -1,9 +1,9 @@
{
"name": "@rhizone-lms/webapp",
- "version": "0.0.0",
+ "version": "0.5.0",
"private": true,
"engines": {
- "node": "^18.14.0",
+ "node": "^18.16.0",
"yarn": "^1.22.19"
},
"scripts": {
@@ -17,32 +17,32 @@
"test": "TZ=UTC react-scripts test"
},
"dependencies": {
- "@babel/core": "^7.20.12",
- "@emotion/react": "^11.5.0",
- "@emotion/styled": "^11.3.0",
- "@mui/icons-material": "^5.11.0",
- "@mui/lab": "^5.0.0-alpha.119",
- "@mui/material": "^5.11.8",
- "@testing-library/dom": "^8.20.0",
+ "@babel/core": "^7.21.8",
+ "@emotion/react": "^11.11.0",
+ "@emotion/styled": "^11.11.0",
+ "@mui/icons-material": "^5.11.16",
+ "@mui/lab": "^5.0.0-alpha.131",
+ "@mui/material": "^5.13.2",
+ "@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^5.11.4",
- "@testing-library/react": "^13.4.0",
+ "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
- "@types/jest": "^29.4.0",
- "@types/luxon": "^3.2.0",
- "@types/node": "^18.13.0",
- "@types/react": "^18.0.27",
- "@types/react-big-calendar": "^1.6.0",
- "@types/react-dom": "^18.0.10",
- "luxon": "^3.2.1",
- "prettier": "^2.8.4",
+ "@types/jest": "^29.5.1",
+ "@types/luxon": "^3.3.0",
+ "@types/node": "^20.2.3",
+ "@types/react": "^18.2.6",
+ "@types/react-big-calendar": "^1.6.3",
+ "@types/react-dom": "^18.2.4",
+ "luxon": "^3.3.0",
+ "prettier": "^2.8.8",
"react": "^18.2.0",
- "react-big-calendar": "^1.6.4",
+ "react-big-calendar": "^1.6.9",
"react-dom": "^18.2.0",
- "react-router-dom": "^6.8.1",
+ "react-router-dom": "^6.11.2",
"react-scripts": "5.0.1",
- "socket.io-client": "^4.6.0",
- "typescript": "^4.9.5",
- "web-vitals": "^3.1.1"
+ "socket.io-client": "^4.6.1",
+ "typescript": "^5.0.4",
+ "web-vitals": "^3.3.1"
},
"eslintConfig": {
"extends": [
diff --git a/webapp/src/components/App.tsx b/webapp/src/components/App.tsx
index a2e90d7e..501fbe39 100644
--- a/webapp/src/components/App.tsx
+++ b/webapp/src/components/App.tsx
@@ -11,6 +11,9 @@ import ReflectionsPage from './ReflectionsPage';
import RequireAuth from './RequireAuth';
import SessionContext from './SessionContext';
import ProgramsPage from './ProgramsPage';
+import AssessmentsListPage from './AssessmentsListPage';
+import AssessmentsDetailPage from './AssessmentDetailPage';
+import AssessmentSubmissionsListPage from './AssessmentSubmissionsListPage';
const App = () => {
const { isAuthenticated } = useContext(SessionContext);
@@ -59,6 +62,30 @@ const App = () => {
}
/>
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
diff --git a/webapp/src/components/AssessmentDetailPage.tsx b/webapp/src/components/AssessmentDetailPage.tsx
new file mode 100644
index 00000000..c83b2662
--- /dev/null
+++ b/webapp/src/components/AssessmentDetailPage.tsx
@@ -0,0 +1,433 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { DateTime } from 'luxon';
+
+import {
+ Alert,
+ AlertTitle,
+ Button,
+ Container,
+ Grid,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ Stack,
+ CircularProgress,
+} from '@mui/material';
+
+import { AssessmentSubmission, SavedAssessment } from '../types/api';
+import useApiData from '../helpers/useApiData';
+
+import AssessmentMetadataBar from './AssessmentMetadataBar';
+import AssessmentDisplay from './AssessmentDisplay';
+import AssessmentSubmitBar from './AssessmentSubmitBar';
+
+const AssessmentDetailPage = () => {
+ const { assessmentId, submissionId } = useParams();
+ const assessmentIdNumber = Number(assessmentId);
+ const submissionIdNumber = Number(submissionId);
+
+ const [apiPath, setApiPath] = useState('');
+ const [assessmentState, setAssessmentState] = useState();
+ const [numOfAnsweredQuestions, setNumOfAnsweredQuestions] = useState(0);
+ const [showSubmitDialog, setShowSubmitDialog] = useState(false);
+ const [endTime, setEndTime] = useState(null);
+ const [secondsRemaining, setSecondsRemaining] = useState(null);
+ const [submissionDisabled, setSubmissionDisabled] = useState(false);
+ const navigate = useNavigate();
+
+ if (Number.isInteger(submissionIdNumber) && submissionIdNumber > 0) {
+ if (apiPath !== `submissions/${submissionIdNumber}`) {
+ setApiPath(`submissions/${submissionIdNumber}`);
+ }
+ } else if (submissionId === 'new') {
+ if (Number.isInteger(assessmentIdNumber) && assessmentIdNumber > 0) {
+ if (apiPath !== `program/${assessmentIdNumber}/submissions/new`) {
+ setApiPath(`program/${assessmentIdNumber}/submissions/new`);
+ }
+ }
+ }
+
+ const {
+ data: assessment,
+ error: getError,
+ isLoading: isLoadingGet,
+ } = useApiData({
+ deps: [],
+ method: 'GET',
+ path: `/assessments/${apiPath}`,
+ sendCredentials: true,
+ });
+
+ const { data: assessmentSubmission, error: putError } =
+ useApiData({
+ body: assessmentState?.submission,
+ deps: [apiPath, assessmentState],
+ method: 'PUT',
+ path: `/assessments/${apiPath}`,
+ sendCredentials: true,
+ shouldFetch: () => {
+ return (
+ typeof assessmentState !== 'undefined' &&
+ assessmentState.submission &&
+ typeof assessmentState.submission !== 'undefined'
+ );
+ },
+ });
+
+ useEffect(() => {
+ if (
+ assessment &&
+ assessment.submission &&
+ typeof assessment.submission !== 'undefined'
+ ) {
+ if (submissionId === 'new') {
+ navigate(
+ `/assessments/${assessmentId}/submissions/${assessment.submission.id}`,
+ { replace: true }
+ );
+ }
+ }
+ }, [assessment, assessmentId, navigate, submissionId]);
+
+ useEffect(() => {
+ if (
+ assessment &&
+ typeof assessment !== 'undefined' &&
+ typeof assessmentState === 'undefined'
+ ) {
+ setAssessmentState(assessment);
+
+ const openedDate = DateTime.fromISO(assessment.submission.opened_at);
+ const dueDate = DateTime.fromISO(assessment.program_assessment.due_date);
+ if (assessment.submission.submitted_at) {
+ setEndTime(DateTime.fromISO(assessment.submission.submitted_at));
+ setSecondsRemaining(null);
+ } else if (
+ assessment.curriculum_assessment.time_limit &&
+ openedDate.plus({
+ minutes: assessment.curriculum_assessment.time_limit,
+ }) < dueDate
+ ) {
+ const newEndTime = openedDate.plus({
+ minutes: assessment.curriculum_assessment.time_limit,
+ });
+ setSecondsRemaining(
+ Math.floor(newEndTime.diff(DateTime.now()).as('seconds'))
+ );
+ setEndTime(newEndTime);
+ } else {
+ setSecondsRemaining(
+ Math.floor(dueDate.diff(DateTime.now()).as('seconds'))
+ );
+ setEndTime(dueDate);
+ }
+
+ if (
+ ['Submitted', 'Graded', 'Expired', 'Disabled'].includes(
+ assessment.submission.assessment_submission_state
+ )
+ ) {
+ setSubmissionDisabled(true);
+ }
+ }
+ }, [assessment, assessmentState]);
+
+ useEffect(() => {
+ if (assessmentSubmission) {
+ if (
+ !['Opened', 'In Progress'].includes(
+ assessmentSubmission.assessment_submission_state
+ )
+ ) {
+ setSubmissionDisabled(true);
+ }
+ }
+ if (
+ assessmentState &&
+ assessmentSubmission &&
+ typeof assessmentSubmission !== 'undefined'
+ ) {
+ if (
+ DateTime.fromISO(assessmentSubmission.last_modified) >
+ DateTime.fromISO(assessmentState.submission.last_modified)
+ ) {
+ const assessmentWithResponses = structuredClone(assessmentState);
+ assessmentWithResponses.submission =
+ structuredClone(assessmentSubmission);
+ assessmentWithResponses.submission.last_modified =
+ assessmentSubmission.last_modified;
+ setAssessmentState(assessmentWithResponses);
+ }
+ }
+ }, [assessmentSubmission, assessmentState]);
+
+ const requestRef = useRef();
+ const previousTimeRef = useRef();
+
+ const animate = (time: number) => {
+ if (previousTimeRef.current !== undefined && endTime) {
+ if (DateTime.now() < endTime) {
+ const newSecondsRemaining = Math.floor(
+ endTime.diff(DateTime.now()).as('seconds')
+ );
+ setSecondsRemaining(newSecondsRemaining);
+ } else {
+ setSecondsRemaining(0);
+ }
+ }
+ previousTimeRef.current = time;
+ if (
+ secondsRemaining !== null &&
+ secondsRemaining > 0 &&
+ !submissionDisabled
+ ) {
+ requestRef.current = requestAnimationFrame(animate);
+ } else {
+ if (
+ assessmentState &&
+ !submissionDisabled &&
+ secondsRemaining === 0 &&
+ assessmentState.submission.assessment_submission_state === 'In Progress'
+ ) {
+ const completedAssessment = structuredClone(assessmentState);
+ completedAssessment.submission.assessment_submission_state = 'Expired';
+ completedAssessment.submission.last_modified =
+ DateTime.now().toISO() || new Date().toISOString();
+ setAssessmentState(completedAssessment);
+ }
+ if (requestRef.current) cancelAnimationFrame(requestRef.current);
+ }
+ };
+
+ useEffect(() => {
+ requestRef.current = requestAnimationFrame(animate);
+ return () => {
+ if (requestRef.current) cancelAnimationFrame(requestRef.current);
+ };
+ });
+
+ const handleUpdatedResponse = (
+ questionId: number,
+ answerId?: number,
+ responseText?: string
+ ) => {
+ if (
+ !(
+ typeof assessmentState !== 'undefined' &&
+ typeof assessmentState.submission.responses !== 'undefined'
+ )
+ ) {
+ return;
+ }
+
+ const [responseQuestion] =
+ assessmentState.curriculum_assessment.questions!.filter(
+ question => question.id === questionId
+ )!;
+
+ const assessmentWithUpdatedResponses = structuredClone(assessmentState);
+
+ if (
+ typeof assessmentWithUpdatedResponses.submission.responses === 'undefined'
+ ) {
+ return;
+ }
+
+ if (responseQuestion.question_type === 'single choice') {
+ assessmentWithUpdatedResponses.submission.responses.find(
+ response => response.question_id === questionId
+ )!.answer_id = answerId;
+ } else if (responseQuestion.question_type === 'free response') {
+ assessmentWithUpdatedResponses.submission.responses.find(
+ response => response.question_id === questionId
+ )!.response_text = responseText;
+ }
+
+ assessmentWithUpdatedResponses.submission.last_modified =
+ DateTime.now().toISO() || new Date().toISOString();
+
+ setNumOfAnsweredQuestions(
+ assessmentWithUpdatedResponses.submission.responses.filter(
+ response =>
+ Number.isInteger(response.answer_id) ||
+ (typeof response.response_text !== 'undefined' &&
+ response.response_text &&
+ response.response_text !== '')
+ ).length
+ );
+
+ if (submissionId === 'new') {
+ setApiPath(`submissions/${assessmentWithUpdatedResponses.submission.id}`);
+ }
+
+ setAssessmentState(assessmentWithUpdatedResponses);
+ };
+
+ const handleSubmit = (event?: React.SyntheticEvent) => {
+ if (event) {
+ event.preventDefault();
+ }
+ if (
+ !(assessmentState && typeof assessmentState.submission !== 'undefined')
+ ) {
+ return;
+ }
+ setShowSubmitDialog(false);
+ if (requestRef.current) cancelAnimationFrame(requestRef.current);
+ const completedAssessment = structuredClone(assessmentState);
+ completedAssessment.submission.assessment_submission_state = 'Submitted';
+ completedAssessment.submission.submitted_at =
+ DateTime.now().toISO() || new Date().toISOString();
+ completedAssessment.submission.last_modified =
+ DateTime.now().toISO() || new Date().toISOString();
+ setSubmissionDisabled(true);
+ setAssessmentState(completedAssessment);
+
+ if (putError) {
+ completedAssessment.submission.assessment_submission_state =
+ 'In Progress';
+ completedAssessment.submission.submitted_at = undefined;
+ setAssessmentState(completedAssessment);
+ setSubmissionDisabled(false);
+ }
+ };
+
+ if (
+ !Number.isInteger(assessmentIdNumber) ||
+ (!Number.isInteger(submissionIdNumber) && submissionId !== 'new') ||
+ assessmentIdNumber < 1 ||
+ (submissionId !== 'new' && submissionIdNumber < 1)
+ ) {
+ return (
+
+
+ Sorry!
+ The assessment ID and submission ID must be valid.
+
+
+
+ );
+ }
+
+ if (
+ (isLoadingGet && typeof assessmentState === 'undefined') ||
+ (typeof assessment !== 'undefined' &&
+ typeof assessmentState === 'undefined')
+ ) {
+ return (
+
+
+
+ );
+ }
+
+ if (
+ getError ||
+ !assessment ||
+ typeof assessmentState === 'undefined' ||
+ typeof assessmentState.curriculum_assessment.questions === 'undefined' ||
+ typeof assessmentState.submission.responses === 'undefined' ||
+ assessmentState.curriculum_assessment.questions.length !==
+ assessmentState.submission.responses.length ||
+ !endTime
+ ) {
+ return (
+
+
+ Error Encountered
+ There was a problem loading this assessment or submission.
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {assessment.curriculum_assessment.title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AssessmentDetailPage;
diff --git a/webapp/src/components/AssessmentDisplay.tsx b/webapp/src/components/AssessmentDisplay.tsx
new file mode 100644
index 00000000..aaa4fa6f
--- /dev/null
+++ b/webapp/src/components/AssessmentDisplay.tsx
@@ -0,0 +1,96 @@
+import React, { useRef, useEffect } from 'react';
+
+import { Alert, AlertTitle, Card, Grid } from '@mui/material';
+
+import { SavedAssessment } from '../types/api.d';
+import AssessmentQuestionCard from './AssessmentQuestionCard';
+
+interface AssessmentsDisplayProps {
+ assessment: SavedAssessment;
+ handleUpdatedResponse: (
+ questionId: number,
+ answerId?: number,
+ responseText?: string
+ ) => void;
+ questionsDisabled: boolean;
+}
+
+const AssessmentDisplay = ({
+ assessment,
+ handleUpdatedResponse,
+ questionsDisabled,
+}: AssessmentsDisplayProps) => {
+ const successMessageRef = useRef(null);
+
+ useEffect(() => {
+ if (
+ assessment.submission.assessment_submission_state === 'Submitted' &&
+ successMessageRef.current
+ ) {
+ successMessageRef.current.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ });
+ }
+ }, [assessment.submission.assessment_submission_state]);
+
+ if (typeof assessment.curriculum_assessment.questions === 'undefined') {
+ return (
+
+
+ Sorry!
+ This assessment contains no questions.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ Success
+ The assessment has been submitted successfully!
+
+
+
+ {assessment.curriculum_assessment.questions
+ .sort(
+ (firstQuestion, secondQuestion) =>
+ firstQuestion.sort_order - secondQuestion.sort_order
+ )
+ .map(question => (
+
+ response.question_id === question.id
+ )[0]
+ }
+ handleUpdatedResponse={handleUpdatedResponse}
+ disabled={questionsDisabled}
+ />
+
+ ))}
+
+ );
+};
+
+export default AssessmentDisplay;
diff --git a/webapp/src/components/AssessmentMetadataBar.tsx b/webapp/src/components/AssessmentMetadataBar.tsx
new file mode 100644
index 00000000..1dac287f
--- /dev/null
+++ b/webapp/src/components/AssessmentMetadataBar.tsx
@@ -0,0 +1,247 @@
+import React, { useState } from 'react';
+
+import {
+ Avatar,
+ Divider,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ Tooltip,
+ Typography,
+ Switch,
+ Button,
+} from '@mui/material';
+import InfoIcon from '@mui/icons-material/Info';
+import TimerIcon from '@mui/icons-material/Timer';
+import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import LightbulbIcon from '@mui/icons-material/Lightbulb';
+
+import { SavedAssessment } from '../types/api.d';
+import { formatDateTime } from '../helpers/dateTime';
+import { DateTime, Duration } from 'luxon';
+
+interface AssessmentMetadataBarProps {
+ assessment: SavedAssessment;
+ endTime: DateTime;
+ secondsRemaining: number | null;
+ submissionDisabled: boolean;
+}
+
+const AssessmentMetadataBar = ({
+ assessment,
+ endTime,
+ secondsRemaining,
+ submissionDisabled,
+}: AssessmentMetadataBarProps) => {
+ const enabledBgColor = '#1e88e5';
+ const disabledBgColor = '#bdbdbd';
+
+ const [showTimer, setShowTimer] = useState(true);
+
+ const handleToggleTimer = () => {
+ setShowTimer(!showTimer);
+ };
+
+ return (
+ <>
+
+ {assessment.curriculum_assessment.description &&
+ typeof assessment.curriculum_assessment.description === 'string' &&
+ assessment.curriculum_assessment.description.length > 0 && (
+ <>
+
+
+
+ {assessment.curriculum_assessment.description}
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {assessment.submission.score &&
+ typeof assessment.submission.score === 'number' &&
+ assessment.submission.score > 0 && (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ {submissionDisabled &&
+ (assessment.submission.submitted_at ? (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ ))}
+ {!submissionDisabled && endTime && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ >
+ );
+};
+
+export default AssessmentMetadataBar;
diff --git a/webapp/src/components/AssessmentQuestionCard.tsx b/webapp/src/components/AssessmentQuestionCard.tsx
new file mode 100644
index 00000000..592acb64
--- /dev/null
+++ b/webapp/src/components/AssessmentQuestionCard.tsx
@@ -0,0 +1,143 @@
+import React, { useState } from 'react';
+import {
+ TextField,
+ FormControlLabel,
+ Radio,
+ Card,
+ CardContent,
+ Typography,
+ RadioGroup,
+ FormControl,
+ MenuItem,
+} from '@mui/material';
+import Select, { SelectChangeEvent } from '@mui/material/Select';
+
+import { Question, AssessmentResponse } from '../types/api';
+
+interface QuestionCardProps {
+ assessmentQuestion: Question;
+ submissionResponse: AssessmentResponse;
+ handleUpdatedResponse: (
+ questionId: number,
+ answerId?: number,
+ responseText?: string
+ ) => void;
+ disabled: boolean;
+}
+
+const AssessmentQuestionCard = ({
+ assessmentQuestion,
+ submissionResponse,
+ handleUpdatedResponse,
+ disabled,
+}: QuestionCardProps) => {
+ const [singleChoiceValue, setSingleChoiceValue] = useState(
+ submissionResponse.answer_id ? submissionResponse.answer_id : 0
+ );
+ const handleChangeSingleChoice = (event: SelectChangeEvent) => {
+ setSingleChoiceValue(Number(event.target.value));
+ handleUpdatedResponse(
+ Number(assessmentQuestion.id),
+ Number(event.target.value)
+ );
+ };
+
+ const [responseTextValue, setResponseTextValue] = useState(
+ submissionResponse.response_text ? submissionResponse.response_text : ''
+ );
+
+ const handleChangeText = (event: React.ChangeEvent) => {
+ setResponseTextValue(event.target.value);
+ handleUpdatedResponse(
+ Number(assessmentQuestion.id),
+ undefined,
+ event.target.value === '' ? undefined : event.target.value
+ );
+ };
+
+ const TableRowWrapper = (question: Question) => {
+ if (
+ assessmentQuestion.question_type === 'single choice' &&
+ assessmentQuestion.answers?.length &&
+ assessmentQuestion.answers?.length < 5
+ ) {
+ return (
+
+
+ {assessmentQuestion.answers?.map(answer => (
+
+ }
+ value={answer.id}
+ key={answer.id}
+ label={answer.title}
+ checked={submissionResponse.answer_id === answer.id}
+ />
+ {answer.description && (
+
+ ({answer.description})
+
+ )}{' '}
+
+ ))}
+
+
+ );
+ } else if (question.question_type === 'single choice') {
+ return (
+
+
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ };
+ return (
+
+
+
+ {assessmentQuestion.sort_order}. {assessmentQuestion.title}
+ {assessmentQuestion.description && (
+
+ ({assessmentQuestion.description})
+
+ )}
+
+ {TableRowWrapper(assessmentQuestion)}
+
+
+ );
+};
+
+export default AssessmentQuestionCard;
diff --git a/webapp/src/components/AssessmentSubmissionsListPage.tsx b/webapp/src/components/AssessmentSubmissionsListPage.tsx
new file mode 100644
index 00000000..976835f9
--- /dev/null
+++ b/webapp/src/components/AssessmentSubmissionsListPage.tsx
@@ -0,0 +1,269 @@
+import React, { useState, useEffect } from 'react';
+import { DateTime } from 'luxon';
+
+import { styled } from '@mui/material/styles';
+import { useParams } from 'react-router-dom';
+import { CircularProgress } from '@mui/material';
+import Button from '@mui/material/Button';
+import Container from '@mui/material/Container';
+import Grid from '@mui/material/Grid';
+import Stack from '@mui/material/Stack';
+import Table from '@mui/material/Table';
+import TableBody from '@mui/material/TableBody';
+import TableCell, { tableCellClasses } from '@mui/material/TableCell';
+import TableContainer from '@mui/material/TableContainer';
+import TableHead from '@mui/material/TableHead';
+import TableRow from '@mui/material/TableRow';
+import Paper from '@mui/material/Paper';
+
+import { formatDateTime } from '../helpers/dateTime';
+import useApiData from '../helpers/useApiData';
+import { AssessmentWithSubmissions } from '../types/api';
+
+import { renderChipByStatus } from './AssessmentsListTable';
+
+const StyledTableCell = styled(TableCell)(({ theme }) => ({
+ [`&.${tableCellClasses.head}`]: {
+ backgroundColor: theme.palette.common.black,
+ color: theme.palette.common.white,
+ textAlign: 'center',
+ },
+ [`&.${tableCellClasses.body}`]: {
+ fontSize: 14,
+ textAlign: 'center',
+ },
+}));
+
+const StyledTableRow = styled(TableRow)(({ theme }) => ({
+ '&:nth-of-type(odd)': {
+ backgroundColor: theme.palette.action.hover,
+ },
+ '&:last-child td, &:last-child th': {
+ border: 0,
+ },
+}));
+
+const AssessmentSubmissionsListPage = () => {
+ const { assessmentId } = useParams();
+ const assessmentIdNumber = Number(assessmentId);
+ const [isFacilitator, setIsFacilitator] = useState(false);
+
+ const {
+ data: assessmentSub,
+ error,
+ isLoading,
+ } = useApiData({
+ deps: [],
+ path: `/assessments/program/${assessmentId}/submissions`,
+ sendCredentials: true,
+ });
+
+ useEffect(() => {
+ if (assessmentSub) {
+ if (assessmentSub.principal_program_role === 'Facilitator') {
+ setIsFacilitator(true);
+ } else {
+ setIsFacilitator(false);
+ }
+ }
+ }, [assessmentSub]);
+
+ if (!assessmentSub || error) {
+ return (
+
+ There was an error loading the assessment submissions list.
+
+
+ );
+ }
+
+ const NewSubmissionButton = () => {
+ if (
+ DateTime.fromISO(assessmentSub.program_assessment.available_after) <
+ DateTime.now() &&
+ DateTime.fromISO(assessmentSub.program_assessment.due_date) >
+ DateTime.now() &&
+ (!assessmentSub.submissions ||
+ assessmentSub.curriculum_assessment.max_num_submissions >
+ assessmentSub.submissions.length) &&
+ !isFacilitator
+ ) {
+ return (
+
+ );
+ }
+ return null;
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ const SubmissionsHeader = () => {
+ return (
+
+ {assessmentSub.curriculum_assessment.title}
+ {assessmentSub.curriculum_assessment.description}
+ {DateTime.now() <
+ DateTime.fromISO(
+ assessmentSub.program_assessment.available_after
+ ) && (
+
+ Available After:{' '}
+ {formatDateTime(assessmentSub.program_assessment.available_after)}
+
+ )}
+
+ Due Date:{' '}
+ {formatDateTime(assessmentSub.program_assessment.due_date)}
+
+
+ );
+ };
+
+ const SubmissionsFooter = () => {
+ return (
+
+
+
+
+
+
+ );
+ };
+
+ if (!assessmentSub.submissions || assessmentSub.submissions.length === 0) {
+ return (
+
+
+
+
+ No submissions for this program assessment.
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {isFacilitator ? (
+ Participant ID
+ ) : null}
+ Submission ID
+ State
+ Opened At
+ Submitted At
+ Score
+ Action
+
+
+
+ {assessmentSub.submissions.map(submission => (
+
+ {isFacilitator ? (
+
+ {submission.principal_id}
+
+ ) : null}
+
+ {submission.id}
+
+
+ {renderChipByStatus(
+ submission.assessment_submission_state
+ )}
+
+
+ {formatDateTime(submission.opened_at)}
+
+
+ {submission.submitted_at &&
+ formatDateTime(submission.submitted_at)}
+
+ {submission.score}
+
+ {isFacilitator ? (
+ submission.assessment_submission_state === 'Graded' ? (
+
+ ) : (
+
+ )
+ ) : ['Opened', 'In Progress'].includes(
+ submission.assessment_submission_state
+ ) ? (
+
+ ) : (
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default AssessmentSubmissionsListPage;
diff --git a/webapp/src/components/AssessmentSubmitBar.tsx b/webapp/src/components/AssessmentSubmitBar.tsx
new file mode 100644
index 00000000..79c73f7a
--- /dev/null
+++ b/webapp/src/components/AssessmentSubmitBar.tsx
@@ -0,0 +1,131 @@
+import React, { Dispatch, SetStateAction } from 'react';
+
+import {
+ Box,
+ Button,
+ Chip,
+ Divider,
+ LinearProgress,
+ List,
+ ListItem,
+ ListItemText,
+ Typography,
+} from '@mui/material';
+import { styled } from '@mui/system';
+
+import { SavedAssessment, Question } from '../types/api.d';
+
+const LinearProgressWithLabel = (
+ numOfAnsweredQuestions: number,
+ numOfTotalQuestions: number
+) => {
+ return (
+
+
+
+
+
+ {`${numOfAnsweredQuestions}/${numOfTotalQuestions}`}
+
+
+ );
+};
+
+const StyledNumChip = styled(Chip)(() => ({
+ borderRadius: '50%',
+ width: '3em',
+ height: '3em',
+}));
+
+interface AssessmentSubmitBarProps {
+ assessment: SavedAssessment;
+ numOfAnsweredQuestions: number;
+ setShowSubmitDialog: Dispatch>;
+ submitButtonDisabled: boolean;
+}
+
+const AssessmentSubmitBar = ({
+ assessment,
+ numOfAnsweredQuestions,
+ setShowSubmitDialog,
+ submitButtonDisabled,
+}: AssessmentSubmitBarProps) => {
+ if (
+ !assessment.curriculum_assessment.questions ||
+ !Array.isArray(assessment.curriculum_assessment.questions) ||
+ !assessment.submission.responses
+ ) {
+ return null;
+ }
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {assessment.curriculum_assessment.questions.map(
+ (question: Question) => (
+ a.question_id === question.id
+ )!.answer_id ||
+ assessment.submission.responses!.find(
+ a => a.question_id === question.id
+ )!.response_text
+ ? 'primary'
+ : 'default'
+ }`}
+ />
+ )
+ )}
+
+
+
+ >
+ );
+};
+
+export default AssessmentSubmitBar;
diff --git a/webapp/src/components/AssessmentsListPage.tsx b/webapp/src/components/AssessmentsListPage.tsx
new file mode 100644
index 00000000..59a0eac1
--- /dev/null
+++ b/webapp/src/components/AssessmentsListPage.tsx
@@ -0,0 +1,183 @@
+import React, { useEffect, useState } from 'react';
+
+import { Container, Stack, CircularProgress } from '@mui/material';
+
+import { AssessmentWithSummary } from '../types/api';
+import AssessmentsListTable from './AssessmentsListTable';
+import AssessmentsListTabs from './AssessmentsListTabs';
+import useApiData from '../helpers/useApiData';
+import { DateTime } from 'luxon';
+
+export enum StatusTab {
+ All,
+ Active,
+ Past,
+ Upcoming,
+}
+
+const AssessmentsListPage = () => {
+ const [currentStatusTab, setCurrentStatusTab] = useState(StatusTab.Active);
+ const [assessmentsListSubset, setAssessmentListSubset] = useState<
+ AssessmentWithSummary[]
+ >([]);
+ const [userRoles, setUserRoles] = useState({
+ isFacilitator: false,
+ isParticipant: false,
+ isMixedRole: false,
+ isNeither: false,
+ });
+
+ const {
+ data: assessmentsList,
+ error,
+ isLoading,
+ } = useApiData({
+ deps: [],
+ path: '/assessments',
+ sendCredentials: true,
+ });
+
+ useEffect(() => {
+ if (!assessmentsList) return;
+
+ const isFacilitator =
+ assessmentsList.filter(
+ assessment => assessment.principal_program_role === 'Facilitator'
+ ).length > 0;
+ const isParticipant =
+ assessmentsList.filter(
+ assessment => assessment.principal_program_role === 'Participant'
+ ).length > 0;
+ const isMixedRole = isFacilitator && isParticipant;
+ const isNeither = !isFacilitator && !isParticipant;
+
+ setUserRoles({ isFacilitator, isParticipant, isMixedRole, isNeither });
+
+ switch (currentStatusTab) {
+ case 1:
+ setAssessmentListSubset(
+ assessmentsList.filter(
+ assessment =>
+ DateTime.now() <
+ DateTime.fromISO(assessment.program_assessment.due_date) &&
+ DateTime.now() >=
+ DateTime.fromISO(assessment.program_assessment.available_after)
+ )
+ );
+ break;
+ case 2:
+ setAssessmentListSubset(
+ assessmentsList.filter(
+ assessment =>
+ DateTime.now() >
+ DateTime.fromISO(assessment.program_assessment.due_date)
+ )
+ );
+ break;
+ case 3:
+ setAssessmentListSubset(
+ assessmentsList.filter(
+ assessment =>
+ DateTime.now() <
+ DateTime.fromISO(assessment.program_assessment.available_after)
+ )
+ );
+ break;
+ default:
+ setAssessmentListSubset(assessmentsList);
+ break;
+ }
+ }, [currentStatusTab, assessmentsList]);
+
+ const handleChangeTab = (
+ event: React.SyntheticEvent,
+ newCurrentStatusTab: number
+ ) => {
+ event.preventDefault();
+ setCurrentStatusTab(newCurrentStatusTab);
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ Assessments
+
+
+ There was an error loading the assessments list.
+
+ );
+ }
+
+ if (!assessmentsList || assessmentsList.length === 0) {
+ return (
+
+
+ Assessments
+
+
+
+ You have no available assessments at this time.{' '}
+
+ Enroll in a demo program as a participant
+ {' '}
+ to see an example assessments list.
+
+
+ );
+ }
+
+ return (
+
+
+ Assessments
+
+
+
+ {assessmentsListSubset.length > 0 ? (
+
+ ) : (
+
+ There are no {StatusTab[currentStatusTab].toLowerCase()} assessments
+ to show at this time.
+
+ )}
+
+ );
+};
+
+export default AssessmentsListPage;
diff --git a/webapp/src/components/AssessmentsListTable.tsx b/webapp/src/components/AssessmentsListTable.tsx
new file mode 100644
index 00000000..32ed012f
--- /dev/null
+++ b/webapp/src/components/AssessmentsListTable.tsx
@@ -0,0 +1,468 @@
+import React from 'react';
+
+import {
+ Chip,
+ Button,
+ Paper,
+ TableRow,
+ TableHead,
+ TableContainer,
+ TableCell,
+ TableBody,
+ Table,
+} from '@mui/material';
+
+import ArrowRightOutlinedIcon from '@mui/icons-material/ArrowRightOutlined';
+import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
+import CheckOutlinedIcon from '@mui/icons-material/CheckOutlined';
+import DoneAllOutlinedIcon from '@mui/icons-material/DoneAllOutlined';
+import LockClockOutlinedIcon from '@mui/icons-material/LockClockOutlined';
+import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
+
+import { formatDateTime } from '../helpers/dateTime';
+import { AssessmentWithSummary } from '../types/api';
+import { StatusTab } from './AssessmentsListPage';
+
+interface PrincipalProgramRole {
+ isFacilitator: boolean;
+ isParticipant: boolean;
+ isMixedRole?: boolean;
+ isNeither?: boolean;
+}
+
+interface TableCellWrapperProps {
+ children?: React.ReactNode;
+ index: number[];
+ statusTab: number;
+ showForFacilitator: boolean;
+ showForParticipant: boolean;
+ principalRoles: PrincipalProgramRole;
+}
+
+const TableCellWrapper = (props: TableCellWrapperProps) => {
+ const {
+ children,
+ statusTab,
+ index,
+ showForFacilitator,
+ showForParticipant,
+ principalRoles,
+ } = props;
+ return index.includes(statusTab) &&
+ ((showForFacilitator &&
+ (principalRoles.isFacilitator || principalRoles.isMixedRole)) ||
+ (showForParticipant &&
+ (principalRoles.isParticipant || principalRoles.isMixedRole))) ? (
+ {children}
+ ) : null;
+};
+
+const renderButtonByStatus = (
+ programAssessmentId: number,
+ principalRole: string,
+ status?: string
+) => {
+ let buttonLabel = '';
+ let destinationPath = '';
+ let displayButton = true;
+ if (principalRole === 'Facilitator') {
+ return (
+
+ );
+ }
+ switch (status) {
+ case 'Active':
+ buttonLabel = 'Start';
+ destinationPath = 'submissions/new';
+ break;
+ case 'Opened':
+ case 'In Progress':
+ buttonLabel = 'Resume';
+ destinationPath = 'submissions/new';
+ break;
+ case 'Submitted':
+ case 'Graded':
+ buttonLabel = 'Review';
+ destinationPath = 'submissions';
+ break;
+ case 'Upcoming':
+ displayButton = false;
+ break;
+ case 'Unsubmitted':
+ case 'Expired':
+ default:
+ buttonLabel = 'Info';
+ destinationPath = 'submissions';
+ break;
+ }
+ if (!displayButton) {
+ return null;
+ }
+ return (
+
+ );
+};
+
+export const renderChipByStatus = (status: string) => {
+ switch (status) {
+ case 'Active':
+ return (
+ }
+ label="Active"
+ />
+ );
+ case 'Opened':
+ case 'In Progress':
+ return (
+ }
+ label={status}
+ />
+ );
+ case 'Submitted':
+ return (
+ }
+ label="Submitted"
+ />
+ );
+ case 'Graded':
+ return (
+ }
+ label="Graded"
+ />
+ );
+ case 'Upcoming':
+ return (
+ }
+ label="Upcoming"
+ />
+ );
+ case 'Expired':
+ return (
+ }
+ label="Expired"
+ />
+ );
+ default:
+ return null;
+ }
+};
+
+interface AssessmentListTableProps {
+ currentStatusTab: StatusTab;
+ matchingAssessmentList: AssessmentWithSummary[];
+ userRoles: PrincipalProgramRole;
+}
+
+const AssessmentsListTable = ({
+ currentStatusTab,
+ matchingAssessmentList,
+ userRoles,
+}: AssessmentListTableProps) => {
+ return (
+
+
+
+
+
+ Assessment Name
+
+
+ Type
+
+
+ Available Date
+
+
+ Due Date
+
+
+ Submitted Date
+
+
+ Participants
+
+
+ Score
+
+
+ Ungraded
+
+
+ State
+
+
+ Action
+
+
+
+
+ {matchingAssessmentList.map(assessment => (
+
+
+ {assessment.curriculum_assessment.title}
+
+ {assessment.curriculum_assessment.description}
+
+
+ {assessment.curriculum_assessment.assessment_type[0].toUpperCase() +
+ assessment.curriculum_assessment.assessment_type.slice(1)}
+
+
+ {formatDateTime(assessment.program_assessment.available_after)}
+
+
+ {formatDateTime(assessment.program_assessment.due_date)}
+
+
+ {assessment.participant_submissions_summary &&
+ assessment.participant_submissions_summary
+ .most_recent_submitted_date
+ ? formatDateTime(
+ assessment.participant_submissions_summary
+ ?.most_recent_submitted_date
+ )
+ : ' '}
+
+
+ {assessment.facilitator_submissions_summary
+ ? `${assessment.facilitator_submissions_summary.num_participants_with_submissions} (of ${assessment.facilitator_submissions_summary.num_program_participants})`
+ : ' '}
+
+
+ {assessment.participant_submissions_summary &&
+ assessment.participant_submissions_summary.highest_score
+ ? assessment.participant_submissions_summary.highest_score
+ : ' '}
+
+
+ {assessment.facilitator_submissions_summary
+ ? assessment.facilitator_submissions_summary
+ .num_ungraded_submissions
+ : ' '}
+
+
+ {assessment.participant_submissions_summary &&
+ assessment.program_assessment.id &&
+ renderChipByStatus(
+ assessment.participant_submissions_summary.highest_state
+ )}
+
+
+ {assessment.participant_submissions_summary &&
+ assessment.program_assessment.id &&
+ renderButtonByStatus(
+ Number(assessment.program_assessment.id),
+ assessment.principal_program_role,
+ assessment.participant_submissions_summary.highest_state
+ )}
+ {assessment.facilitator_submissions_summary &&
+ assessment.program_assessment.id &&
+ renderButtonByStatus(
+ Number(assessment.program_assessment.id),
+ assessment.principal_program_role
+ )}
+
+
+ ))}
+
+
+
+ );
+};
+
+export default AssessmentsListTable;
diff --git a/webapp/src/components/AssessmentsListTabs.tsx b/webapp/src/components/AssessmentsListTabs.tsx
new file mode 100644
index 00000000..51d5ef85
--- /dev/null
+++ b/webapp/src/components/AssessmentsListTabs.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+
+import { Box, Tabs, Tab } from '@mui/material';
+import Badge, { BadgeProps } from '@mui/material/Badge';
+import { styled } from '@mui/material/styles';
+
+import ArchiveOutlinedIcon from '@mui/icons-material/ArchiveOutlined';
+import AssessmentIcon from '@mui/icons-material/Assessment';
+import UpcomingOutlinedIcon from '@mui/icons-material/UpcomingOutlined';
+import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined';
+
+import { AssessmentWithSummary } from '../types/api';
+import { StatusTab } from './AssessmentsListPage';
+
+const StyledBadge = styled(Badge)(({ theme }) => ({
+ '& .MuiBadge-badge': {
+ border: `2px solid ${theme.palette.background.paper}`,
+ padding: '0 4px',
+ },
+}));
+
+interface AssessmentsListTabsProps {
+ assessmentList: AssessmentWithSummary[];
+ currentStatusTab: StatusTab;
+ handleChangeTab: (
+ event: React.SyntheticEvent,
+ newCurrentStatusTab: number
+ ) => void;
+}
+
+const AssessmentsListTabs = ({
+ assessmentList,
+ currentStatusTab,
+ handleChangeTab,
+}: AssessmentsListTabsProps) => {
+ return (
+
+
+
+
+
+ }
+ iconPosition="start"
+ label="All"
+ />
+
+ assessment.principal_program_role === 'Participant' &&
+ assessment.participant_submissions_summary
+ ?.highest_state === 'Active'
+ ).length
+ }
+ color="primary"
+ >
+
+
+ }
+ iconPosition="start"
+ label="Active"
+ />
+
+
+
+ }
+ iconPosition="start"
+ label="Past"
+ />
+
+
+
+ }
+ iconPosition="start"
+ label="Upcoming"
+ />
+
+
+ );
+};
+
+export default AssessmentsListTabs;
diff --git a/webapp/src/components/Navbar.tsx b/webapp/src/components/Navbar.tsx
index 247f00cd..1e5ea944 100644
--- a/webapp/src/components/Navbar.tsx
+++ b/webapp/src/components/Navbar.tsx
@@ -4,6 +4,7 @@ import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import HomeIcon from '@mui/icons-material/Home';
import PeopleIcon from '@mui/icons-material/People';
import EngineeringIcon from '@mui/icons-material/Engineering';
+import AssessmentIcon from '@mui/icons-material/Assessment';
import { Button, Grid, IconButton, Tooltip } from '@mui/material';
import styled from '@emotion/styled';
@@ -51,11 +52,19 @@ const Navbar = () => {
+
+
+
+
+
+
+
+