UNCLASSIFIED

You need to sign in or sign up before continuing.
Commits (3)
...@@ -366,36 +366,16 @@ ...@@ -366,36 +366,16 @@
}, },
"dependencies": { "dependencies": {
"browserslist": { "browserslist": {
"version": "4.16.7", "version": "4.16.8",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.7.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz",
"integrity": "sha512-7I4qVwqZltJ7j37wObBe3SoTz+nS8APaNcrBOlgoirb6/HbEU2XxW/LpUDTCngM6iauwFqmRTuOMfyKnFGY5JA==", "integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30001248", "caniuse-lite": "^1.0.30001251",
"colorette": "^1.2.2", "colorette": "^1.3.0",
"electron-to-chromium": "^1.3.793", "electron-to-chromium": "^1.3.811",
"escalade": "^3.1.1", "escalade": "^3.1.1",
"node-releases": "^1.1.73" "node-releases": "^1.1.75"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
"dev": true
},
"electron-to-chromium": {
"version": "1.3.807",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.807.tgz",
"integrity": "sha512-p8uxxg2a23zRsvQ2uwA/OOI+O4BQxzaR7YKMIGGGQCpYmkFX2CVF5f0/hxLMV7yCr7nnJViCwHLhPfs52rIYCA==",
"dev": true
},
"node-releases": {
"version": "1.1.74",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.74.tgz",
"integrity": "sha512-caJBVempXZPepZoZAPCWRTNxYQ+xtG/KAi4ozTA5A+nJ7IU+kLQCbqaUjb5Rwy14M9upBWiQ4NutcmW04LJSRw==",
"dev": true
}
} }
}, },
"caniuse-lite": { "caniuse-lite": {
...@@ -3941,36 +3921,16 @@ ...@@ -3941,36 +3921,16 @@
}, },
"dependencies": { "dependencies": {
"browserslist": { "browserslist": {
"version": "4.16.7", "version": "4.16.8",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.7.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz",
"integrity": "sha512-7I4qVwqZltJ7j37wObBe3SoTz+nS8APaNcrBOlgoirb6/HbEU2XxW/LpUDTCngM6iauwFqmRTuOMfyKnFGY5JA==", "integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30001248", "caniuse-lite": "^1.0.30001251",
"colorette": "^1.2.2", "colorette": "^1.3.0",
"electron-to-chromium": "^1.3.793", "electron-to-chromium": "^1.3.811",
"escalade": "^3.1.1", "escalade": "^3.1.1",
"node-releases": "^1.1.73" "node-releases": "^1.1.75"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30001251",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001251.tgz",
"integrity": "sha512-HOe1r+9VkU4TFmnU70z+r7OLmtR+/chB1rdcJUeQlAinjEeb0cKL20tlAtOagNZhbrtLnCvV19B4FmF1rgzl6A==",
"dev": true
},
"electron-to-chromium": {
"version": "1.3.807",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.807.tgz",
"integrity": "sha512-p8uxxg2a23zRsvQ2uwA/OOI+O4BQxzaR7YKMIGGGQCpYmkFX2CVF5f0/hxLMV7yCr7nnJViCwHLhPfs52rIYCA==",
"dev": true
},
"node-releases": {
"version": "1.1.74",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.74.tgz",
"integrity": "sha512-caJBVempXZPepZoZAPCWRTNxYQ+xtG/KAi4ozTA5A+nJ7IU+kLQCbqaUjb5Rwy14M9upBWiQ4NutcmW04LJSRw==",
"dev": true
}
} }
}, },
"caniuse-lite": { "caniuse-lite": {
...@@ -9253,6 +9213,11 @@ ...@@ -9253,6 +9213,11 @@
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true "dev": true
}, },
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"path-to-regexp": { "path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
...@@ -9576,6 +9541,11 @@ ...@@ -9576,6 +9541,11 @@
} }
} }
}, },
"private": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz",
"integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg=="
},
"process-nextick-args": { "process-nextick-args": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
......
...@@ -132,7 +132,6 @@ ...@@ -132,7 +132,6 @@
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-notifier": "^9.0.0", "node-notifier": "^9.0.0",
"normalize-url": "^4.5.1", "normalize-url": "^4.5.1",
"path-parse": "^1.0.7",
"prismjs": "^1.24.0", "prismjs": "^1.24.0",
"ua-parser-js": "^0.7.23", "ua-parser-js": "^0.7.23",
"url-parse": "^1.5.0", "url-parse": "^1.5.0",
......
...@@ -84,6 +84,7 @@ export const parseCourseQueryParams = (req, res, next) => { ...@@ -84,6 +84,7 @@ export const parseCourseQueryParams = (req, res, next) => {
completedCount, completedCount,
instructors, instructors,
registrations, registrations,
pendingRegistrations,
attendance, attendance,
instructorOf, instructorOf,
} = req.query; } = req.query;
...@@ -95,6 +96,7 @@ export const parseCourseQueryParams = (req, res, next) => { ...@@ -95,6 +96,7 @@ export const parseCourseQueryParams = (req, res, next) => {
...(completedCount === 'true' && { completedCount: true }), ...(completedCount === 'true' && { completedCount: true }),
...(instructors === 'true' && { instructors: true }), ...(instructors === 'true' && { instructors: true }),
...(registrations === 'true' && { registrations: true }), ...(registrations === 'true' && { registrations: true }),
...(pendingRegistrations === 'true' && { pendingRegistrations: true }),
...(attendance === 'true' && { attendance: true }), ...(attendance === 'true' && { attendance: true }),
...(instructorOf === 'true' && { instructorOf: true }), ...(instructorOf === 'true' && { instructorOf: true }),
}; };
...@@ -186,6 +188,7 @@ export const coursesResponse = (req, res, coursesResult, resultsAttrName) => { ...@@ -186,6 +188,7 @@ export const coursesResponse = (req, res, coursesResult, resultsAttrName) => {
registeredCount, registeredCount,
instructors, instructors,
registrations, registrations,
pendingRegistrations,
attendance, attendance,
instructorOf, instructorOf,
} = req.parsedQueryParams; } = req.parsedQueryParams;
...@@ -203,6 +206,7 @@ export const coursesResponse = (req, res, coursesResult, resultsAttrName) => { ...@@ -203,6 +206,7 @@ export const coursesResponse = (req, res, coursesResult, resultsAttrName) => {
registeredCount, registeredCount,
instructors, instructors,
registrations, registrations,
pendingRegistrations,
attendance, attendance,
instructorOf, instructorOf,
total: coursesResult?.count || 0, total: coursesResult?.count || 0,
......
import { authz, permit } from '../auth/authservice-middleware';
import Permission from '../auth/permission';
import { jsonResponse } from '../lib/request-response-helpers';
import {
idParameter,
idBody,
requiredBodyField,
requiredStringBody,
} from '../lib/validate-sanitize';
import CourseService from '../services/course-service';
import {
checkCourseInstructorPermissions,
parseCourseQueryParams,
coursesResponse,
registrationsResponse,
parseRegistrationSearchQueryParams,
} from './courses-middleware';
// gets pending registrations for a course
// open to admins and super admins, as well as the instructors of the specified course
export const getCoursePendingRegistrationsApi = [
authz,
checkCourseInstructorPermissions,
idParameter('courseId'),
parseRegistrationSearchQueryParams,
async (req, res, next) => {
try {
const { courseId } = req.params;
const result = await CourseService.getPendingRegistrations(
courseId,
req.parsedQueryParams
);
registrationsResponse(req, res, result);
} catch (err) {
next(err);
}
},
];
// place user in pending course registration
// open to admins and PMs
export const postPendingCourseRegistrationsApi = [
authz,
permit([Permission.ADMIN, Permission.SUPER_ADMIN]),
[idParameter('courseId'), idBody('userId'), idBody('pmId')],
async (req, res, next) => {
try {
const { courseId } = req.params;
const { userId, pmId } = req.body;
const result = await CourseService.addPendingCourseRegistration(
courseId,
userId,
pmId
);
jsonResponse(res, result);
} catch (err) {
next(err);
}
},
];
// remove course pending registration
// open to admins and super admins, as well as the instructors of the specified course
export const deletePendingCourseRegistrationsApi = [
authz,
checkCourseInstructorPermissions,
[idParameter('courseId'), idParameter('userId')],
async (req, res, next) => {
try {
const { courseId, userId } = req.params;
await CourseService.removePedningCourseRegistration(courseId, userId);
jsonResponse(res, { delete: true });
} catch (err) {
next(err);
}
},
];
// restore course pending registration
// open to admins and super admins, as well as the instructors of the specified course
export const restorePendingCourseRegistrationsApi = [
authz,
checkCourseInstructorPermissions,
[idParameter('courseId'), idParameter('userId')],
async (req, res, next) => {
try {
const { courseId, userId } = req.params;
await CourseService.restorePendingCourseRegistration(courseId, userId);
jsonResponse(res, { restore: true });
} catch (err) {
next(err);
}
},
];
// get all courses a user is registered for
// open to only admins and super admins
export const getPendingCourseRegistrationsByUserApi = [
permit([Permission.ADMIN, Permission.SUPER_ADMIN]),
idParameter('userId'),
parseCourseQueryParams,
async (req, res, next) => {
try {
const { userId } = req.params;
const result = await CourseService.getPendingRegistrationsByUser(
userId,
req.parsedQueryParams
);
coursesResponse(req, res, result, 'pending registrations');
} catch (err) {
next(err);
}
},
];
// get all courses for the calling user
// open to all users
export const getPendingCourseRegistrationsByCurrentUserApi = [
authz,
parseCourseQueryParams,
async (req, res, next) => {
try {
const userId = req.user.id;
const result = await CourseService.getPendingRegistrationsByUser(
userId,
req.parsedQueryParams
);
coursesResponse(req, res, result, 'pending registrations');
} catch (err) {
next(err);
}
},
];
// get all pending courses by PM Id
// open to only admins and super admins
export const getPendingCourseRegistrationsByPmApi = [
permit([Permission.ADMIN, Permission.SUPER_ADMIN]),
idParameter('pmId'),
parseCourseQueryParams,
async (req, res, next) => {
try {
const { pmId } = req.params;
const result = await CourseService.getPendingRegistrationsByPM(
pmId,
req.parsedQueryParams
);
coursesResponse(req, res, result, 'pending registrations');
} catch (err) {
next(err);
}
},
];
// set course attendance
// open to admins and super admins, as well as the instructors of the specified course
export const putNotficationApi = [
authz,
checkCourseInstructorPermissions,
[
idParameter('courseId'),
idBody('userId'),
idBody('pmId'),
requiredStringBody('mainText', 3, 2000),
requiredBodyField('add'),
],
async (req, res, next) => {
try {
const { courseId } = req.params;
const { mainText, subText, userId, pmId, add } = req.body;
const msgObj = { mainText };
if (subText) {
msgObj.subText = subText;
}
const result = await CourseService.setNotification(
courseId,
userId,
pmId,
add,
msgObj
);
jsonResponse(res, result);
} catch (err) {
next(err);
}
},
];
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
} from '../lib/validate-sanitize'; } from '../lib/validate-sanitize';
import CourseService from '../services/course-service'; import CourseService from '../services/course-service';
import * as registrationApi from './courses-registration-api'; import * as registrationApi from './courses-registration-api';
import * as pendingRegistrationApi from './courses-pending-registration-api';
import * as instructorApi from './courses-instructor-api'; import * as instructorApi from './courses-instructor-api';
import * as certificateApi from './courses-certificate-api'; import * as certificateApi from './courses-certificate-api';
import { import {
...@@ -385,6 +386,39 @@ coursesApi.get( ...@@ -385,6 +386,39 @@ coursesApi.get(
'/my-registrations', '/my-registrations',
registrationApi.getCourseRegistrationsByCurrentUserApi registrationApi.getCourseRegistrationsByCurrentUserApi
); );
// pending registrations
coursesApi.get(
'/:courseId(\\d+)/pending-registrations',
pendingRegistrationApi.getCoursePendingRegistrationsApi
);
coursesApi.post(
'/:courseId(\\d+)/pending-registrations',
pendingRegistrationApi.postPendingCourseRegistrationsApi
);
coursesApi.delete(
'/:courseId(\\d+)/pending-registrations/:userId(\\d+)',
pendingRegistrationApi.deletePendingCourseRegistrationsApi
);
coursesApi.put(
'/:courseId(\\d+)/pending-registrations/:userId(\\d+)/restore',
pendingRegistrationApi.restorePendingCourseRegistrationsApi
);
coursesApi.get(
'/pending-registrations/by-user/:userId(\\d+)',
pendingRegistrationApi.getPendingCourseRegistrationsByUserApi
);
coursesApi.get(
'/pending-registrations/by-pm/:pmId(\\d+)',
pendingRegistrationApi.getPendingCourseRegistrationsByPmApi
);
coursesApi.get(
'/my-pending-registrations',
pendingRegistrationApi.getPendingCourseRegistrationsByCurrentUserApi
);
coursesApi.put(
'/:courseId(\\d+)/pending-registrations/acceptance',
pendingRegistrationApi.putNotficationApi
);
// instructors // instructors
coursesApi.get( coursesApi.get(
'/:courseId(\\d+)/instructors', '/:courseId(\\d+)/instructors',
......
...@@ -5,7 +5,12 @@ import Permission from '../auth/permission'; ...@@ -5,7 +5,12 @@ import Permission from '../auth/permission';
import UserService from '../services/user-service'; import UserService from '../services/user-service';
import { parseUserQueryParams, usersResponse } from './users-middleware'; import { parseUserQueryParams, usersResponse } from './users-middleware';
import { isBodyFieldArray } from '../lib/validate-sanitize'; import {
// idBody,
isBodyFieldArray,
// requiredBodyField,
idParameter,
} from '../lib/validate-sanitize';
// apis defined here will resolve at /users/ // apis defined here will resolve at /users/
const usersApi = new Router(); const usersApi = new Router();
...@@ -97,4 +102,41 @@ usersApi.post('/preferences', authz, async (req, res, next) => { ...@@ -97,4 +102,41 @@ usersApi.post('/preferences', authz, async (req, res, next) => {
} }
}); });
// get notifications for user
usersApi.get('/notifications', authz, async (req, res, next) => {
try {
const notifications = await UserService.getUserNotifications(req.user.id);
jsonResponse(res, notifications || {});
} catch (err) {
next(err);
}
});
usersApi.delete(
'/notification/:notificationId(\\d+)',
idParameter('notificationId'),
authz,
async (req, res, next) => {
try {
const { notificationId } = req.params;
await UserService.deleteUserNotification(req.user.id, notificationId);
jsonResponse(res, { delete: true });
} catch (err) {
next(err);
}
}
);
usersApi.put(
'/notifications/:notificationId(\\d+)/restore',
idParameter('notificationId'),
authz,
async (req, res, next) => {
try {
const { notificationId } = req.params;
await UserService.restoreUserNotifications(req.user.id, notificationId);
jsonResponse(res, { restore: true });
} catch (err) {
next(err);
}
}
);
export default usersApi; export default usersApi;
...@@ -14,35 +14,36 @@ const isDuplicateRecord = async (model, sequelize) => { ...@@ -14,35 +14,36 @@ const isDuplicateRecord = async (model, sequelize) => {
//iterate through the unique fields and create a search object set to the values in the model //iterate through the unique fields and create a search object set to the values in the model
const searchObject = {}; const searchObject = {};
for (const field of uniqueFields) { if (uniqueFields) {
searchObject[field] = model.dataValues[field]; for (const field of uniqueFields) {
} searchObject[field] = model.dataValues[field];
}
/* using the unique fields search with the model and the set values to see if there is a duplicate object /* using the unique fields search with the model and the set values to see if there is a duplicate object
that has not been deleted */ that has not been deleted */
const notEqualToModelInstance = { const notEqualToModelInstance = {
id: { id: {
[Sequelize.Op.ne]: model.dataValues.id, [Sequelize.Op.ne]: model.dataValues.id,
}, },
}; };
const isFound = await sequelize.model(modelName).findOne({ const isFound = await sequelize.model(modelName).findOne({
where: { where: {
[Sequelize.Op.and]: [ [Sequelize.Op.and]: [
searchObject, searchObject,
{ {
deleted_at: { deleted_at: {
[Sequelize.Op.eq]: null, [Sequelize.Op.eq]: null,
},
}, },
}, notEqualToModelInstance,
notEqualToModelInstance, ],
], },
}, });
});
if (isFound) { if (isFound) {
isDuplicate = true; isDuplicate = true;
}
} }
//return boolean //return boolean
return isDuplicate; return isDuplicate;
}; };
......
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction();
try {
// user_alerts
await queryInterface.createTable(
'user_notifications',
{
id: {
type: Sequelize.DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
user_id: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
main_text: Sequelize.DataTypes.TEXT,
sub_text: Sequelize.DataTypes.TEXT,
deleted_at: Sequelize.DataTypes.DATE,
created_at: Sequelize.DataTypes.DATE,
updated_at: Sequelize.DataTypes.DATE,
},
{
transaction,
}
);
await queryInterface.createTable(
'course_pending_registrations',
{
id: {
type: Sequelize.DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
user_id: {
type: Sequelize.DataTypes.INTEGER,
unique: 'compositeIndex',
allowNull: false,
references: {
model: 'users',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
pm_id: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false,
references: {
model: 'users',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
course_id: {
type: Sequelize.DataTypes.INTEGER,
unique: 'compositeIndex',
allowNull: false,
references: {
model: 'courses',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
message: Sequelize.DataTypes.TEXT,
notification_message: {
allowNull: true,
type: Sequelize.DataTypes.BOOLEAN,
defaultValue: null,
},
deleted_at: Sequelize.DataTypes.DATE,
created_at: Sequelize.DataTypes.DATE,
updated_at: Sequelize.DataTypes.DATE,
},
{
transaction,
uniqueKeys: {
registrations_unique: {
fields: ['user_id', 'course_id'],
},
},
}
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
down: async (queryInterface) => {
const transaction = await queryInterface.sequelize.transaction();
try {
// drops tables that were created in the migration up
await queryInterface.dropTable('course_pending_registrations', {
transaction,
});
await queryInterface.dropTable('user_notifications', {
transaction,
});
await transaction.commit();
} catch (err) {
await transaction.rollback();
throw err;
}
},
};
import db from '../config/dbConfig';
import Sequelize from 'sequelize';
class CoursePendingRegistrations extends Sequelize.Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
CoursePendingRegistrations.Course = CoursePendingRegistrations.belongsTo(models['Course'], {
as: 'course',
foreignKey: 'courseId',
});
CoursePendingRegistrations.User = CoursePendingRegistrations.belongsTo(models['User'], {
as: 'user',
foreignKey: 'userId',
});
CoursePendingRegistrations.PM = CoursePendingRegistrations.belongsTo(models['User'], {
as: 'pm',
foreignKey: 'pmId',
});
}
}
CoursePendingRegistrations.init(
{
userId: Sequelize.DataTypes.INTEGER,
pmId: Sequelize.DataTypes.INTEGER,
courseId: Sequelize.DataTypes.INTEGER,
},
{
paranoid: true,
sequelize: db,
}
);
CoursePendingRegistrations.uniqueFields = ['userId', 'courseId'];
export default CoursePendingRegistrations;
...@@ -59,6 +59,10 @@ class Course extends Sequelize.Model { ...@@ -59,6 +59,10 @@ class Course extends Sequelize.Model {
as: 'registrations', as: 'registrations',
foreignKey: 'courseId', foreignKey: 'courseId',
}); });
Course.PendingRegistrations = Course.hasMany(models['CoursePendingRegistrations'], {
as: 'pendingRegistrations',
foreignKey: 'courseId',
});
Course.Instructors = Course.hasMany(models['CourseInstructor'], { Course.Instructors = Course.hasMany(models['CourseInstructor'], {
as: 'instructors', as: 'instructors',
foreignKey: 'courseId', foreignKey: 'courseId',
......
// import all models // import all models
import CourseRegistration from './course-registration'; import CourseRegistration from './course-registration';
import CoursePendingRegistrations from './course-pending-registrations';
import CourseInstructor from './course-instructor'; import CourseInstructor from './course-instructor';
import CourseAttendance from './course-attendance'; import CourseAttendance from './course-attendance';
import Course from './course'; import Course from './course';
...@@ -7,6 +8,7 @@ import Team from './team'; ...@@ -7,6 +8,7 @@ import Team from './team';
import TeamMember from './team-member'; import TeamMember from './team-member';
import User from './user'; import User from './user';
import UserPreference from './user-preference'; import UserPreference from './user-preference';
import UserNotifications from './user-notifications';
import Sequelize from 'sequelize'; import Sequelize from 'sequelize';
import sequelize from '../config/dbConfig'; import sequelize from '../config/dbConfig';
...@@ -15,11 +17,13 @@ const db = { ...@@ -15,11 +17,13 @@ const db = {
Course, Course,
CourseAttendance, CourseAttendance,
CourseRegistration, CourseRegistration,
CoursePendingRegistrations,
CourseInstructor, CourseInstructor,
Team, Team,
TeamMember, TeamMember,
User, User,
UserPreference, UserPreference,
UserNotifications,
}; };
// set up associations // set up associations
......
import db from '../config/dbConfig';
import Sequelize from 'sequelize';
class UserNotifications extends Sequelize.Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
UserNotifications.belongsTo(models['User']);
}
}
UserNotifications.init(
{
userId: Sequelize.DataTypes.INTEGER,
mainText: Sequelize.DataTypes.STRING,
subText: Sequelize.DataTypes.STRING,
},
{
paranoid: true,
sequelize: db,
}
);
export default UserNotifications;
...@@ -75,6 +75,14 @@ class User extends Sequelize.Model { ...@@ -75,6 +75,14 @@ class User extends Sequelize.Model {
as: 'courseRegistrations', as: 'courseRegistrations',
foreignKey: 'userId', foreignKey: 'userId',
}); });
User.hasMany(models['CoursePendingRegistrations'], {
as: 'coursePendingRegistrations',
foreignKey: 'userId',
});
User.hasMany(models['UserNotifications'], {
as: 'userNotifications',
foreignKey: 'userId',
});
User.hasMany(models['CourseInstructor'], { User.hasMany(models['CourseInstructor'], {
as: 'instructorOfCourses', as: 'instructorOfCourses',
foreignKey: 'userId', foreignKey: 'userId',
......
...@@ -7,6 +7,8 @@ import DuplicateObjectError from '../errors/DuplicateObjectError'; ...@@ -7,6 +7,8 @@ import DuplicateObjectError from '../errors/DuplicateObjectError';
import User from '../models/user'; import User from '../models/user';
import UserService from './user-service'; import UserService from './user-service';
import CourseRegistration from '../models/course-registration'; import CourseRegistration from '../models/course-registration';
import CoursePendingRegistrations from '../models/course-pending-registrations';
import UserNotifications from '../models/user-notifications';
import CapacityLimitError from '../errors/CapacityLimitError'; import CapacityLimitError from '../errors/CapacityLimitError';
import CourseAttendance from '../models/course-attendance'; import CourseAttendance from '../models/course-attendance';
import db from '../config/dbConfig'; import db from '../config/dbConfig';
...@@ -27,6 +29,10 @@ export const mergeUserIntoInstructors = (course) => { ...@@ -27,6 +29,10 @@ export const mergeUserIntoInstructors = (course) => {
} }
}; };
const orderParseHelper = (searchParams) => {
return searchParams.order?.map((o) => [literal(`\`user.${o[0]}\``), o[1]]);
};
const difference = (setA, setB) => { const difference = (setA, setB) => {
const _difference = new Set(setA); const _difference = new Set(setA);
for (const elem of setB) { for (const elem of setB) {
...@@ -137,6 +143,12 @@ export default class CourseService { ...@@ -137,6 +143,12 @@ export default class CourseService {
if (searchParams.registeredCount || searchParams.completedCount) { if (searchParams.registeredCount || searchParams.completedCount) {
query.include.push({ model: CourseRegistration, as: 'registrations' }); query.include.push({ model: CourseRegistration, as: 'registrations' });
} }
if (searchParams.pendingRegistrations) {
query.include.push({
model: CoursePendingRegistrations,
as: 'pendingRegistrations',
});
}
if (searchParams.instructorOf) { if (searchParams.instructorOf) {
query.include.push({ query.include.push({
model: CourseInstructor, model: CourseInstructor,
...@@ -182,7 +194,12 @@ export default class CourseService { ...@@ -182,7 +194,12 @@ export default class CourseService {
*/ */
static async getCourseById( static async getCourseById(
courseId, courseId,
{ instructors = false, registrations = false, attendance = false } = {} {
instructors = false,
pendingRegistrations = false,
registrations = false,
attendance = false,
} = {}
) { ) {
// set associated models to return // set associated models to return
const include = []; const include = [];
...@@ -199,6 +216,22 @@ export default class CourseService { ...@@ -199,6 +216,22 @@ export default class CourseService {
], ],
}); });
} }
if (pendingRegistrations) {
include.push({
model: CoursePendingRegistrations,
as: 'pendingRegistrations',
include: [
{
model: UserService.protectUserData(User),
as: 'user',
},
{
model: UserService.protectUserData(User),
as: 'pm',
},
],
});
}
if (registrations || attendance) { if (registrations || attendance) {
// include the registrations relation // include the registrations relation
const registrationModel = { const registrationModel = {
...@@ -273,6 +306,12 @@ export default class CourseService { ...@@ -273,6 +306,12 @@ export default class CourseService {
}, },
transaction, transaction,
}); });
await CoursePendingRegistrations.destroy({
where: {
courseId,
},
transaction,
});
await CourseInstructor.destroy({ await CourseInstructor.destroy({
where: { where: {
courseId, courseId,
...@@ -297,6 +336,12 @@ export default class CourseService { ...@@ -297,6 +336,12 @@ export default class CourseService {
}, },
transaction, transaction,
}); });
await CoursePendingRegistrations.restore({
where: {
courseId: id,
},
transaction,
});
await CourseInstructor.restore({ await CourseInstructor.restore({
where: { where: {
courseId: id, courseId: id,
...@@ -420,6 +465,17 @@ export default class CourseService { ...@@ -420,6 +465,17 @@ export default class CourseService {
userId, userId,
}); });
} }
static async addPendingCourseRegistration(courseId, userId, pmId) {
const course = await CourseService.getCourseById(courseId, {
pendingRegistrations: true,
});
return CoursePendingRegistrations.create({
courseId: course.id,
userId,
pmId,
});
}
static async updateCourseRegistration(courseId, userId, { completed }) { static async updateCourseRegistration(courseId, userId, { completed }) {
const registration = await CourseRegistration.findOne({ const registration = await CourseRegistration.findOne({
...@@ -439,6 +495,14 @@ export default class CourseService { ...@@ -439,6 +495,14 @@ export default class CourseService {
}, },
}); });
} }
static async removePedningCourseRegistration(courseId, userId) {
return CoursePendingRegistrations.destroy({
where: {
courseId,
userId,
},
});
}
static async restoreCourseRegistration(courseId, userId) { static async restoreCourseRegistration(courseId, userId) {
return CourseRegistration.restore({ return CourseRegistration.restore({
...@@ -448,12 +512,21 @@ export default class CourseService { ...@@ -448,12 +512,21 @@ export default class CourseService {
}, },
}); });
} }
static async restorePendingCourseRegistration(courseId, userId) {
return CoursePendingRegistrations.restore({
where: {
courseId,
userId,
},
});
}
static async getRegistrationsByUser(userId, searchParams = {}) { static async getRegistrationsByUser(userId, searchParams = {}) {
const user = await UserService.getUserById(userId); const user = await UserService.getUserById(userId);
const courseWhere = const courseWhere = convertCourseSearchParamsToSequelizeSearch(
convertCourseSearchParamsToSequelizeSearch(searchParams); searchParams
);
const include = [ const include = [
{ {
model: Course, model: Course,
...@@ -488,6 +561,64 @@ export default class CourseService { ...@@ -488,6 +561,64 @@ export default class CourseService {
}); });
} }
static async getPendingRegistrationsByUser(userId, searchParams = {}) {
const user = await UserService.getUserById(userId);
const courseWhere = convertCourseSearchParamsToSequelizeSearch(
searchParams
);
const include = [
{
model: Course,
as: 'course',
where: courseWhere,
},
];
const registrationsWhere = { userId: user.id };
return CoursePendingRegistrations.findAndCountAll({
where: registrationsWhere,
attributes: {
exclude: ['courseId', 'userId', 'pmId'],
},
include,
// have to prefix order object with the Course model since we are including Course
order: searchParams.order?.map((o) => [Course, ...o]),
offset: searchParams.offset,
limit: searchParams.pageSize,
});
}
static async getPendingRegistrationsByPM(pmId, searchParams = {}) {
const user = await UserService.getUserById(pmId);
const courseWhere = convertCourseSearchParamsToSequelizeSearch(
searchParams
);
const include = [
{
model: Course,
as: 'course',
where: courseWhere,
},
];
const registrationsWhere = { pmId: user.id };
return CoursePendingRegistrations.findAndCountAll({
where: registrationsWhere,
attributes: {
exclude: ['courseId', 'userId', 'pmId'],
},
include,
// have to prefix order object with the Course model since we are including Course
order: searchParams.order?.map((o) => [Course, ...o]),
offset: searchParams.offset,
limit: searchParams.pageSize,
});
}
static async getRegistrations(courseId, searchParams) { static async getRegistrations(courseId, searchParams) {
const usersWhere = convertUsersSearchParamsToSequelizeSearch(searchParams); const usersWhere = convertUsersSearchParamsToSequelizeSearch(searchParams);
...@@ -514,10 +645,28 @@ export default class CourseService { ...@@ -514,10 +645,28 @@ export default class CourseService {
// not sure why the order has to be done like this, but `user`.`name` isn't a valid // not sure why the order has to be done like this, but `user`.`name` isn't a valid
// column when the attendance association is included in the query. weird thing in // column when the attendance association is included in the query. weird thing in
// sequelize i suppose // sequelize i suppose
order: searchParams.order?.map((o) => [ order: orderParseHelper(searchParams),
literal(`\`user.${o[0]}\``), offset: searchParams.offset,
o[1], limit: searchParams.pageSize,
]), });
}
static async getPendingRegistrations(courseId, searchParams) {
const include = [
{
model: UserService.protectUserData(User),
as: 'user',
},
{
model: UserService.protectUserData(User),
as: 'pm',
},
];
return CoursePendingRegistrations.findAndCountAll({
where: { courseId },
include,
distinct: true,
order: orderParseHelper(searchParams),
offset: searchParams.offset, offset: searchParams.offset,
limit: searchParams.pageSize, limit: searchParams.pageSize,
}); });
...@@ -602,4 +751,51 @@ export default class CourseService { ...@@ -602,4 +751,51 @@ export default class CourseService {
throw err; throw err;
} }
} }
static async setNotification(courseId, userId, pmId, add, txtObj) {
const transaction = await db.transaction();
try {
const pendingRegistartion = await CoursePendingRegistrations.findOne({
where: { courseId, userId },
});
// ensure the Pedning registration exists
if (pendingRegistartion === null) {
throw new ObjectNotFoundError(
`Course pending registration not found for user id ${userId}`
);
}
const course = await CourseService.getCourseById(courseId, {
registrations: true,
});
// validate the course is not at max capacity
if (course.registrations.length >= course.capacity) {
throw new CapacityLimitError('Course is already at maximum capacity');
}
const result = [];
if (add) {
result.push(
await CourseRegistration.create(
{
courseId,
userId,
},
{ transaction }
)
);
}
result.push(
await UserNotifications.create(
{ userId: pmId, ...txtObj },
{ transaction }
)
);
result.push(await pendingRegistartion.destroy({ transaction }));
await transaction.commit();
return result;
} catch (err) {
await transaction.rollback();
throw err;
}
}
} }
...@@ -3,6 +3,7 @@ import User from '../models/user'; ...@@ -3,6 +3,7 @@ import User from '../models/user';
import Permission from '../auth/permission'; import Permission from '../auth/permission';
import ObjectNotFoundError from '../errors/ObjectNotFoundError'; import ObjectNotFoundError from '../errors/ObjectNotFoundError';
import UserPreference from '../models/user-preference'; import UserPreference from '../models/user-preference';
import UserNotifications from '../models/user-notifications';
import NotAuthorizedError from '../errors/NotAuthorizedError'; import NotAuthorizedError from '../errors/NotAuthorizedError';
import db from '../config/dbConfig'; import db from '../config/dbConfig';
import UserPersonnelType from '../lib/constants/user-personnel-type'; import UserPersonnelType from '../lib/constants/user-personnel-type';
...@@ -91,6 +92,38 @@ export default class UserService { ...@@ -91,6 +92,38 @@ export default class UserService {
} }
return user; return user;
} }
// get all notifications by user
static async getUserNotifications(id) {
return UserNotifications.findAndCountAll({ where: { userId: id } });
}
//delete notification
static async deleteUserNotification(userId, id) {
const notification = await UserNotifications.findByPk(id);
if (notification?.userId === userId) {
return notification.destroy();
} else {
throw notification
? new NotAuthorizedError(
'Current User does not match Notification User'
)
: new ObjectNotFoundError('Notification not found');
}
}
//restore notifictaion
static async restoreUserNotifications(userId, id) {
const notification = await UserNotifications.findByPk(id, {
paranoid: false,
});
if (notification?.userId === userId) {
return notification.restore();
} else {
throw notification
? new NotAuthorizedError(
'Current User does not match Notification User'
)
: new ObjectNotFoundError('Notification not found');
}
}
static async getUserPreferences(user) { static async getUserPreferences(user) {
if (Number.isInteger(user)) { if (Number.isInteger(user)) {
......
This diff is collapsed.
...@@ -500,4 +500,94 @@ describe('/users', () => { ...@@ -500,4 +500,94 @@ describe('/users', () => {
expect(response.type).toEqual('application/json'); expect(response.type).toEqual('application/json');
}); });
}); });
describe('GET /notifications', () => {
it('should return 500 if getUserNotifications fails', async () => {
console.error = jest.fn(); // used to silence error message
UserService.getUserNotifications = jest
.fn()
.mockRejectedValue('mock-error');
const req = request(app).get('/users/notifications');
const response = await createRequestForMockUser(req);
expect(response.body).toEqual({ message: 'Internal Server Error' });
expect(response.status).toEqual(500);
expect(response.type).toEqual('application/json');
});
it('should get user notifications from service', async () => {
const mockNotification = {
notificationId: 1,
userId: 11,
alerUser: true,
};
UserService.getUserNotifications = jest
.fn()
.mockResolvedValue(mockNotification);
const req = request(app).get('/users/notifications');
const response = await createRequestForMockUser(req);
expect(response.body).toEqual({ result: mockNotification });
expect(response.status).toEqual(200);
expect(response.type).toEqual('application/json');
});
it('should get empty object user notifications from service', async () => {
UserService.getUserNotifications = jest.fn().mockResolvedValue(null);
const req = request(app).get('/users/notifications');
const response = await createRequestForMockUser(req);
expect(response.body).toEqual({ result: {} });
expect(response.status).toEqual(200);
expect(response.type).toEqual('application/json');
});
});
describe('delete /notification/:notificationId', () => {
it('should return 500 if deleteUserNotification fails', async () => {
console.error = jest.fn(); // used to silence error message
UserService.deleteUserNotification = jest
.fn()
.mockRejectedValue('mock-error');
const req = request(app).delete('/users/notification/1');
const response = await createRequestForMockUser(req);
expect(response.body).toEqual({ message: 'Internal Server Error' });
expect(response.status).toEqual(500);
expect(response.type).toEqual('application/json');
});
it('should delete notification', async () => {
UserService.deleteUserNotification = jest
.fn()
.mockResolvedValue({ delete: true });
const req = request(app).delete('/users/notification/1');
const response = await createRequestForMockUser(req);
expect(response.body).toEqual({ result: { delete: true } });
expect(response.status).toEqual(200);
expect(response.type).toEqual('application/json');
});
});
describe('PUT /notifications/:notificationId/restore', () => {
it('should return 500 if restoreUserNotifications fails', async () => {
console.error = jest.fn(); // used to silence error message
UserService.restoreUserNotifications = jest
.fn()
.mockRejectedValue('mock-error');
const req = request(app).put('/users/notifications/1/restore');
const response = await createRequestForMockUser(req);
expect(response.body).toEqual({ message: 'Internal Server Error' });
expect(response.status).toEqual(500);
expect(response.type).toEqual('application/json');
});
it('should restore notification', async () => {
UserService.restoreUserNotifications = jest
.fn()
.mockResolvedValue({ restore: true });
const req = request(app).put('/users/notifications/1/restore');
const response = await createRequestForMockUser(req);
expect(response.body).toEqual({ result: { restore: true } });
expect(response.status).toEqual(200);
expect(response.type).toEqual('application/json');
});
});
}); });
...@@ -8,6 +8,7 @@ import ObjectNotFoundError from '../../src/errors/ObjectNotFoundError'; ...@@ -8,6 +8,7 @@ import ObjectNotFoundError from '../../src/errors/ObjectNotFoundError';
import CapacityLimitError from '../../src/errors/CapacityLimitError'; import CapacityLimitError from '../../src/errors/CapacityLimitError';
import DuplicateObjectError from '../../src/errors/DuplicateObjectError'; import DuplicateObjectError from '../../src/errors/DuplicateObjectError';
import CourseRegistration from '../../src/models/course-registration'; import CourseRegistration from '../../src/models/course-registration';
import CoursePendingRegistrations from '../../src/models/course-pending-registrations';
import UserService from '../../src/services/user-service'; import UserService from '../../src/services/user-service';
import * as streaming from '../../src/lib/streaming'; import * as streaming from '../../src/lib/streaming';
import CourseAttendance from '../../src/models/course-attendance'; import CourseAttendance from '../../src/models/course-attendance';
...@@ -15,6 +16,7 @@ import InvalidInputError from '../../src/errors/invalid-input-error'; ...@@ -15,6 +16,7 @@ import InvalidInputError from '../../src/errors/invalid-input-error';
import db from '../../src/config/dbConfig'; import db from '../../src/config/dbConfig';
import sequelize from '../../src/config/dbConfig'; import sequelize from '../../src/config/dbConfig';
import CourseInstructor from '../../src/models/course-instructor'; import CourseInstructor from '../../src/models/course-instructor';
import UserNotifications from '../../src/models/user-notifications';
describe('Courses Service', () => { describe('Courses Service', () => {
beforeEach(() => { beforeEach(() => {
...@@ -406,6 +408,32 @@ describe('Courses Service', () => { ...@@ -406,6 +408,32 @@ describe('Courses Service', () => {
order: [], order: [],
}); });
}); });
it('given pending registration parameter, should get course with registration', async () => {
Course.findByPk = jest.fn().mockResolvedValue(mockCourse);
User.scope = jest.fn().mockResolvedValue(User);
const result = await CourseService.getCourseById(111, {
pendingRegistrations: true,
});
expect(result).toEqual(mockCourse);
expect(Course.findByPk).toHaveBeenCalledTimes(1);
expect(Course.findByPk).toHaveBeenCalledWith(111, {
include: [
{
model: CoursePendingRegistrations,
as: 'pendingRegistrations',
include: [
{ model: UserService.protectUserData(User), as: 'user' },
{
model: UserService.protectUserData(User),
as: 'pm',
},
],
},
],
order: [],
});
});
it('given attendance parameter, should get course with attendance', async () => { it('given attendance parameter, should get course with attendance', async () => {
Course.findByPk = jest.fn().mockResolvedValue(mockCourse); Course.findByPk = jest.fn().mockResolvedValue(mockCourse);
User.scope = jest.fn().mockResolvedValue(User); User.scope = jest.fn().mockResolvedValue(User);
...@@ -470,6 +498,7 @@ describe('Courses Service', () => { ...@@ -470,6 +498,7 @@ describe('Courses Service', () => {
Course.findByPk = jest.fn().mockResolvedValue(mockCourse); Course.findByPk = jest.fn().mockResolvedValue(mockCourse);
CourseRegistration.destroy = jest.fn(); CourseRegistration.destroy = jest.fn();
CourseInstructor.destroy = jest.fn(); CourseInstructor.destroy = jest.fn();
CoursePendingRegistrations.destroy = jest.fn();
await CourseService.removeCourseById(111); await CourseService.removeCourseById(111);
expect(mockCourse.destroy).toHaveBeenCalledTimes(1); expect(mockCourse.destroy).toHaveBeenCalledTimes(1);
expect(sequelize.transaction).toHaveBeenCalled(); expect(sequelize.transaction).toHaveBeenCalled();
...@@ -487,6 +516,7 @@ describe('Courses Service', () => { ...@@ -487,6 +516,7 @@ describe('Courses Service', () => {
Course.findByPk = jest.fn().mockResolvedValue(mockCourse); Course.findByPk = jest.fn().mockResolvedValue(mockCourse);
CourseRegistration.destroy = jest.fn().mockRejectedValue(true); CourseRegistration.destroy = jest.fn().mockRejectedValue(true);
CoursePendingRegistrations.destroy = jest.fn();
CourseInstructor.destroy = jest.fn(); CourseInstructor.destroy = jest.fn();
try { try {
await CourseService.removeCourseById(111); await CourseService.removeCourseById(111);
...@@ -510,6 +540,9 @@ describe('Courses Service', () => { ...@@ -510,6 +540,9 @@ describe('Courses Service', () => {
}); });
CourseRegistration.restore = jest.fn().mockResolvedValue(mockCourse); CourseRegistration.restore = jest.fn().mockResolvedValue(mockCourse);
CoursePendingRegistrations.restore = jest
.fn()
.mockResolvedValue(mockCourse);
CourseInstructor.restore = jest.fn().mockResolvedValue(mockCourse); CourseInstructor.restore = jest.fn().mockResolvedValue(mockCourse);
Course.restore = jest.fn().mockResolvedValue(mockCourse); Course.restore = jest.fn().mockResolvedValue(mockCourse);
...@@ -1064,6 +1097,314 @@ describe('Courses Service', () => { ...@@ -1064,6 +1097,314 @@ describe('Courses Service', () => {
}); });
}); });
}); });
describe('pending Registrations', () => {
describe('add pending registration', () => {
it('given user is not pending registration, create Pending Registration', async () => {
CourseService.getCourseById = jest.fn().mockResolvedValue({ id: 42 });
CoursePendingRegistrations.create = jest.fn();
UserService.getUserById = jest
.fn()
.mockResolvedValue({ id: 24, name: 'mock-user' });
await CourseService.addPendingCourseRegistration(42, 24, 10);
expect(CoursePendingRegistrations.create).toHaveBeenCalledTimes(1);
expect(CoursePendingRegistrations.create).toHaveBeenCalledWith({
courseId: 42,
userId: 24,
pmId: 10,
});
});
});
describe('remove pending course registration', () => {
it('given course and user exist, should remove pending registration', async () => {
CoursePendingRegistrations.destroy = jest.fn();
const mockCourse = { id: 42 };
CourseService.getCourseById = jest.fn().mockResolvedValue(mockCourse);
const mockUser = { id: 24, name: 'mock-user' };
UserService.getUserById = jest.fn().mockResolvedValue(mockUser);
await CourseService.removePedningCourseRegistration(
mockCourse.id,
mockUser.id
);
expect(CoursePendingRegistrations.destroy).toHaveBeenCalledTimes(1);
expect(CoursePendingRegistrations.destroy).toHaveBeenCalledWith({
where: {
courseId: mockCourse.id,
userId: mockUser.id,
},
});
});
});
describe('restore course pending registration', () => {
it('given course and user exist, should restore pending registration', async () => {
const mockCourse = { id: 42 };
const mockUser = { id: 24, name: 'mock-user' };
CoursePendingRegistrations.restore = jest
.fn()
.mockResolvedValue(mockUser);
await CourseService.restorePendingCourseRegistration(
mockCourse.id,
mockUser.id
);
sequelize.query = jest.fn();
expect(CoursePendingRegistrations.restore).toHaveBeenCalledTimes(1);
});
});
describe('get pending registrations for course', () => {
const mockRegistrationResults = {
count: 2,
rows: [
{ name: 'mock registration 1' },
{ name: 'mock registration 2' },
],
};
it('given no search params, should search pending registrations', async () => {
CoursePendingRegistrations.findAndCountAll = jest
.fn()
.mockResolvedValue(mockRegistrationResults);
User.scope = jest.fn().mockResolvedValue(User);
const searchParams = {};
const results = await CourseService.getPendingRegistrations(
42,
searchParams
);
expect(results).toEqual(mockRegistrationResults);
expect(
CoursePendingRegistrations.findAndCountAll
).toHaveBeenCalledTimes(1);
expect(CoursePendingRegistrations.findAndCountAll).toHaveBeenCalledWith(
{
where: { courseId: 42 },
distinct: true,
include: [
{
model: UserService.protectUserData(User),
as: 'user',
},
{
model: UserService.protectUserData(User),
as: 'pm',
},
],
limit: undefined,
offset: undefined,
order: undefined,
}
);
});
it('given order param, should search pending registrations and sort', async () => {
CoursePendingRegistrations.findAndCountAll = jest
.fn()
.mockResolvedValue(mockRegistrationResults);
User.scope = jest.fn().mockResolvedValue(User);
const searchParams = { order: [['name', 'asc']] };
const results = await CourseService.getPendingRegistrations(
42,
searchParams
);
expect(results).toEqual(mockRegistrationResults);
expect(
CoursePendingRegistrations.findAndCountAll
).toHaveBeenCalledTimes(1);
expect(CoursePendingRegistrations.findAndCountAll).toHaveBeenCalledWith(
{
distinct: true,
where: { courseId: 42 },
include: [
{
model: UserService.protectUserData(User),
as: 'user',
},
{
model: UserService.protectUserData(User),
as: 'pm',
},
],
order: [[{ val: '`user.name`' }, 'asc']],
}
);
});
});
describe('set notification', () => {
const setNoticeMockPayload = [
1,
11,
10,
false,
{ mainText: 'Mock Text Test' },
];
const notificationMockResponse = {
userId: 11,
courseId: 1,
mainText: 'mock text',
};
it('given pending registration does not exists, should throw ObjectNotFoundError', async () => {
CoursePendingRegistrations.findOne = jest.fn().mockResolvedValue(null);
await expect(
CourseService.setNotification(...setNoticeMockPayload)
).rejects.toThrowError(ObjectNotFoundError);
});
it('given course is at capacity, should throw CapacityLimitError', async () => {
jest.mock('../../src/config/dbConfig');
const mockCommit = jest.fn().mockResolvedValue();
const mockRollback = jest.fn().mockResolvedValue();
db.transaction = jest.fn().mockResolvedValue({
commit: mockCommit,
rollback: mockRollback,
});
CoursePendingRegistrations.findOne = jest
.fn()
.mockResolvedValue(notificationMockResponse);
CourseService.getCourseById = jest
.fn()
.mockResolvedValue({ id: 42, capacity: 3, registrations: [1, 2, 3] });
UserService.getUserById = jest
.fn()
.mockResolvedValue({ id: 24, name: 'mock-user' });
await expect(
CourseService.setNotification(...setNoticeMockPayload)
).rejects.toThrowError(CapacityLimitError);
expect(mockCommit).toHaveBeenCalledTimes(0);
expect(mockRollback).toHaveBeenCalledTimes(1);
});
it('given correct args and add: false, it should create User Notification', async () => {
jest.mock('../../src/config/dbConfig');
const mockCommit = jest.fn().mockResolvedValue();
const mockRollback = jest.fn().mockResolvedValue();
db.transaction = jest.fn().mockResolvedValue({
commit: mockCommit,
rollback: mockRollback,
});
CoursePendingRegistrations.findOne = jest.fn().mockResolvedValue({
...notificationMockResponse,
destroy: jest.fn().mockResolvedValue({}),
});
CourseService.getCourseById = jest
.fn()
.mockResolvedValue({ id: 42, capacity: 3, registrations: [1, 2] });
UserNotifications.create = jest.fn().mockResolvedValue({});
const result = await CourseService.setNotification(
...setNoticeMockPayload
);
expect(mockCommit).toHaveBeenCalledTimes(1);
expect(mockRollback).toHaveBeenCalledTimes(0);
expect(result).toEqual([{}, {}]);
});
it('given correct args and add: true, it should create User Notification and add Course Registrations', async () => {
jest.mock('../../src/config/dbConfig');
const mockCommit = jest.fn().mockResolvedValue();
const mockRollback = jest.fn().mockResolvedValue();
db.transaction = jest.fn().mockResolvedValue({
commit: mockCommit,
rollback: mockRollback,
});
CoursePendingRegistrations.findOne = jest.fn().mockResolvedValue({
...notificationMockResponse,
destroy: jest.fn().mockResolvedValue({}),
});
CourseService.getCourseById = jest
.fn()
.mockResolvedValue({ id: 42, capacity: 3, registrations: [1, 2] });
UserNotifications.create = jest.fn().mockResolvedValue({});
CourseRegistration.create = jest.fn().mockResolvedValue({});
const result = await CourseService.setNotification(1, 11, 10, true, {
mainText: 'Mock Text Test',
subtText: 'mock sub text',
});
expect(CourseRegistration.create).toHaveBeenCalledTimes(1);
expect(UserNotifications.create).toHaveBeenCalledWith(
{ userId: 10, mainText: 'Mock Text Test', subtText: 'mock sub text' },
{ transaction: { commit: mockCommit, rollback: mockRollback } }
);
expect(mockCommit).toHaveBeenCalledTimes(1);
expect(mockRollback).toHaveBeenCalledTimes(0);
expect(result).toEqual([{}, {}, {}]);
});
});
describe('get pending registrations by user', () => {
it('given user exists, should search pending registrations by userId', async () => {
const mockUser = { id: 24, name: 'mock-user' };
UserService.getUserById = jest.fn().mockResolvedValue(mockUser);
const mockRegistrations = [{}, {}, {}];
CoursePendingRegistrations.findAndCountAll = jest
.fn()
.mockResolvedValue(mockRegistrations);
const results = await CourseService.getPendingRegistrationsByUser(24);
expect(results).toEqual(mockRegistrations);
expect(
CoursePendingRegistrations.findAndCountAll
).toHaveBeenCalledTimes(1);
expect(CoursePendingRegistrations.findAndCountAll).toHaveBeenCalledWith(
expect.objectContaining({
where: { userId: 24 },
})
);
});
it('given user exists, should search pending registrations by pmId', async () => {
const mockUser = { id: 24, name: 'mock-user' };
UserService.getUserById = jest.fn().mockResolvedValue(mockUser);
const mockRegistrations = [{}, {}, {}];
CoursePendingRegistrations.findAndCountAll = jest
.fn()
.mockResolvedValue(mockRegistrations);
const results = await CourseService.getPendingRegistrationsByPM(24);
expect(results).toEqual(mockRegistrations);
expect(
CoursePendingRegistrations.findAndCountAll
).toHaveBeenCalledTimes(1);
expect(CoursePendingRegistrations.findAndCountAll).toHaveBeenCalledWith(
expect.objectContaining({
where: { pmId: 24 },
})
);
});
it('given order params, should pass order to db call, pending registrations by User', async () => {
const mockUser = { id: 24, name: 'mock-user' };
UserService.getUserById = jest.fn().mockResolvedValue(mockUser);
const mockRegistrations = [{}, {}, {}];
CoursePendingRegistrations.findAndCountAll = jest
.fn()
.mockResolvedValue(mockRegistrations);
const order = [['id', 'asc']];
const results = await CourseService.getPendingRegistrationsByUser(24, {
order,
});
expect(results).toEqual(mockRegistrations);
expect(
CoursePendingRegistrations.findAndCountAll
).toHaveBeenCalledTimes(1);
expect(CoursePendingRegistrations.findAndCountAll).toHaveBeenCalledWith(
expect.objectContaining({
where: { userId: 24 },
include: [
{
model: Course,
as: 'course',
where: {
[Op.and]: {},
},
},
],
order: [[Course, 'id', 'asc']],
})
);
});
});
});
describe('instructors', () => { describe('instructors', () => {
describe('is user course instructor', () => { describe('is user course instructor', () => {
......
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import User from '../../src/models/user'; import User from '../../src/models/user';
import UserPreference from '../../src/models/user-preference'; import UserPreference from '../../src/models/user-preference';
import UserNotifications from '../../src/models/user-notifications';
import UserService from '../../src/services/user-service'; import UserService from '../../src/services/user-service';
import ObjectNotFoundError from '../../src/errors/ObjectNotFoundError'; import ObjectNotFoundError from '../../src/errors/ObjectNotFoundError';
import NotAuthorizedError from '../../src/errors/NotAuthorizedError'; import NotAuthorizedError from '../../src/errors/NotAuthorizedError';
...@@ -486,4 +487,77 @@ describe('UserService', () => { ...@@ -486,4 +487,77 @@ describe('UserService', () => {
expect(response).toEqual(100); expect(response).toEqual(100);
}); });
}); });
describe('user notifications', () => {
const mockFindAllResult = {
total: 2,
rows: [{}, {}],
};
const mockFindbyPkResult = {
id: 11,
userId: 11,
destroy: jest.fn(),
update: jest.fn(),
restore: jest.fn(),
};
it('given userId, should should get all Notifications', async () => {
UserNotifications.findAndCountAll = jest
.fn()
.mockResolvedValue(mockFindAllResult);
const result = await UserService.getUserNotifications(11);
expect(result).toEqual(mockFindAllResult);
expect(UserNotifications.findAndCountAll).toHaveBeenCalledTimes(1);
expect(UserNotifications.findAndCountAll).toHaveBeenCalledWith({
where: { userId: 11 },
});
});
it('given userId and notificationId, should delete Notification', async () => {
UserNotifications.findByPk = jest
.fn()
.mockResolvedValue(mockFindbyPkResult);
await UserService.deleteUserNotification(11, 11);
expect(UserNotifications.findByPk).toHaveBeenCalledTimes(1);
expect(UserNotifications.findByPk).toHaveBeenCalledWith(11);
expect(mockFindbyPkResult.destroy).toHaveBeenCalledTimes(1);
});
it('given userId and notificationId, should restore Notification', async () => {
UserNotifications.findByPk = jest
.fn()
.mockResolvedValue(mockFindbyPkResult);
await UserService.restoreUserNotifications(11, 11);
expect(UserNotifications.findByPk).toHaveBeenCalledTimes(1);
expect(UserNotifications.findByPk).toHaveBeenCalledWith(11, {
paranoid: false,
});
expect(mockFindbyPkResult.restore).toHaveBeenCalledWith();
expect(mockFindbyPkResult.restore).toHaveBeenCalledTimes(1);
});
it('given wrong userId and notificationId for restore, should throw NotAuthorizedError', async () => {
UserNotifications.findByPk = jest
.fn()
.mockResolvedValue(mockFindbyPkResult);
await expect(
UserService.restoreUserNotifications(10, 11)
).rejects.toThrowError(NotAuthorizedError);
});
it('given wrong userId and notificationId, should throw NotAuthorizedError', async () => {
UserNotifications.findByPk = jest
.fn()
.mockResolvedValue(mockFindbyPkResult);
await expect(
UserService.deleteUserNotification(10, 11)
).rejects.toThrowError(NotAuthorizedError);
});
it('given wrong notificationId, should throw ObectNotFoundError', async () => {
UserNotifications.findByPk = jest.fn().mockResolvedValue(null);
await expect(
UserService.deleteUserNotification(11, 10)
).rejects.toThrowError(ObjectNotFoundError);
});
it('given wrong notificationId for restore, should throw ObectNotFoundError', async () => {
UserNotifications.findByPk = jest.fn().mockResolvedValue(null);
await expect(
UserService.restoreUserNotifications(11, 10)
).rejects.toThrowError(ObjectNotFoundError);
});
});
}); });