@universis/evaluations
Version:
Universis evaluations library
319 lines (313 loc) • 16.7 kB
JavaScript
import {EdmMapping, EdmType} from '@themost/data';
import EvaluationEvent from "./evaluation-event-model";
import {DataError, TraceUtils, DataNotFoundError} from "@themost/common";
import moment from "moment";
/**
* @class
*/
.entityType('ClassInstructorEvaluationEvent')
class ClassInstructorEvaluationEvent extends EvaluationEvent {
/**
* @constructor
*/
constructor() {
super();
}
.func('Results', EdmType.CollectionOf('Object'))
async getResults() {
// check access to ClassInstructorEvaluationEvent and get results silently
const event = await this.context.model('ClassInstructorEvaluationEvent').where('id').equal(this.getId());
if (event) {
const form = await this.getForm();
return this.context.model(form.properties.name).where('evaluationEvent').equal(this.id).silent().prepare();
}
}
.action('GenerateEvaluationTokens', 'GenerateInstructorTokenAction')
async generateEvaluationTokens() {
const context = this.context;
const action = {
event: this.id,
numberOfStudents: 0,
total: 0,
actionStatus: {alternateName: 'PotentialActionStatus'}
};
// get courseClass instructor
const event = await context.model('ClassInstructorEvaluationEvent').where('id').equal(this.id)
.expand({
name: 'courseClassInstructor',
options: {
'$expand': 'courseClass'
}
})
.getItem();
if (event) {
//check if a GenerateInstructorTokenAction exists
const prevAction = await context.model('GenerateInstructorTokenAction').where('event').equal(this.id)
.and('actionStatus/alternateName').notEqual('FailedActionStatus')
.silent().getItem();
if (prevAction) {
throw new Error('A previous action for generating evaluation tokens has been found.');
}
const courseClass = event.courseClassInstructor && event.courseClassInstructor.courseClass;
const eventInstructor = event.courseClassInstructor && event.courseClassInstructor.instructor;
// get class sections
const sections = await context.model('CourseClassSection').where('courseClass').equal(courseClass.id).getItems();
// check if course class has sections
if (sections && sections.length > 0 && courseClass.mustRegisterSection !== 0) {
// get only number of students registered at specific section
const instructorSections = await context.model('CourseClassSectionInstructor').where('courseClass').equal(courseClass.id)
.and('instructor').equal(eventInstructor).select('section/section as sectionId').silent().getItems();
if (instructorSections && instructorSections.length === 0) {
action.actionStatus.alternateName = 'FailedActionStatus';
action.description = 'Instructor is not related with any of sections';
return await context.model('GenerateInstructorTokenAction').save(action);
} else {
const sectionIds = instructorSections.map(section => {
return section.sectionId;
});
// get number of students for specific course class sections
action.numberOfStudents = await context.model('StudentCourseClass').where('courseClass').equal(courseClass.id)
.and('section').in(sectionIds)
.silent().count();
}
} else {
action.numberOfStudents = await context.model('StudentCourseClass').where('courseClass').equal(courseClass.id).silent().count();
}
// save action and then generateTokens
const generateAction = await context.model('GenerateInstructorTokenAction').save(action);
const generateOptions =
{
evaluationEvent: event.id,
n: action.numberOfStudents,
expires: event.endDate
};
try {
// save number of students to event
event.numberOfStudents = action.numberOfStudents;
await context.model('ClassInstructorEvaluationEvent').save(event);
if (event.numberOfStudents > 0) {
await EvaluationEvent.generateTokens(context, generateOptions);
}
generateAction.total = action.numberOfStudents;
generateAction.actionStatus = {alternateName: 'CompletedActionStatus'}
return await context.model('GenerateInstructorTokenAction').save(generateAction);
}
catch (err) {
TraceUtils.error(err);
// update also action status
generateAction.total = 0;
generateAction.actionStatus = {alternateName: 'FailedActionStatus'}
generateAction.description = err.message;
return await context.model('GenerateInstructorTokenAction').save(generateAction);
}
}
}
.func('TokenInfo', EdmType.CollectionOf('Object'))
async getTokenStatistics() {
const tokenInfo = {
total: 0,
used: 0,
sent: 0
};
// get number if tokens
const tokens = await this.context.model('EvaluationAccessToken').where('evaluationEvent').equal(this.getId())
.select('count(evaluationEvent) as total', 'used', 'sent')
.groupBy('used', 'sent', 'evaluationEvent')
.silent().getItems();
tokens.map(item => {
tokenInfo.total += item.total;
tokenInfo.used += item.used ? item.total : 0;
tokenInfo.sent += item.sent ? item.total : 0;
});
return tokenInfo;
}
.action('SendEvaluationTokens', 'SendInstructorTokenAction')
async sendEvaluationTokens() {
const context = this.context;
let sendAction = {};
let subject;
// get event and check if event endDate has passed
try {
// get current date
const currentDate = moment(new Date()).startOf('day').toDate(); // get only date format
const event = await context.model('ClassInstructorEvaluationEvent').where('id').equal(this.getId())
.and('date(endDate)').greaterOrEqual(currentDate)
.expand({
name: 'courseClassInstructor',
options: {
'$expand': 'courseClass($expand=course), instructor' // expand course and instructor so their attributes are available in the mail template
}
},
{
name: 'actions',
options: {
'$expand': 'actionStatus'
}
})
.getItem();
// validate event
if (event == null) {
throw new DataNotFoundError('The event cannot be found or is inaccessible.');
}
// validate status
if (!(event.eventStatus && event.eventStatus.alternateName === 'EventOpened')) {
throw new DataError('E_INVALID_PERIOD', context.__('Send tokens action may only be executed for Open/Active events'));
}
// validate event actions
if (!(event.actions && event.actions.length)) {
throw new DataNotFoundError('E_NO_TOKENS', context.__('Token actions were not found for the specified event or they are inaccessible.'));
}
// find completed token generation action (can only be one)
const successfulTokenAction = event.actions.find(generateTokenAction => generateTokenAction.actionStatus.alternateName === 'CompletedActionStatus');
if (successfulTokenAction == null) {
throw new DataError('E_NO_TOKENS', 'The specified event does not have a completed token generation action.');
}
// get tokenInfo for this event
const tokenInfo = await this.getTokenStatistics();
// validate sent-used separately for descriptive errors
if (tokenInfo.sent) {
throw new DataError('E_ALREADY_SENT', `${context.__('The bulk sending of the tokens could not be completed because some of them had already been sent')} (${tokenInfo.sent})`);
}
if (tokenInfo.used) {
throw new DataError('E_ALREADY_USED', `${context.__('The bulk sending of the tokens could not be completed because some of them had already been used')} (${tokenInfo.used})`);
}
// start gathering students
let students;
const courseClass = event.courseClassInstructor && event.courseClassInstructor.courseClass;
const eventInstructor = event.courseClassInstructor && event.courseClassInstructor.instructor;
// get class sections
const sections = await context.model('CourseClassSection').where('courseClass').equal(courseClass.id).getAllItems();
// check if course class has sections
if (sections && sections.length > 0 && courseClass.mustRegisterSection !== 0) {
// get only number of students registered at specific section
const instructorSections = await context.model('CourseClassSectionInstructor').where('courseClass').equal(courseClass.id)
.and('instructor').equal(eventInstructor).select('section/section as sectionId').silent().getAllItems();
if (instructorSections && instructorSections.length === 0) {
throw new DataError('E_INVALID_SECTION', 'Instructor is not related with any of sections');
}
const sectionIds = instructorSections.map(section => {
return section.sectionId;
});
// get students for specific course class sections
students = await context.model('StudentCourseClass')
.where('courseClass').equal(courseClass.id)
.and('section').in(sectionIds)
.select('email')
.silent()
.getAllItems();
} else {
students = await context.model('StudentCourseClass')
.where('courseClass').equal(courseClass.id)
.select('email', 'student')
.silent()
.getAllItems();
}
if (!(Array.isArray(students) && students.length)) {
throw new DataError('E_STUDENTS_FOUND', 'No recipients/students found.');
}
// check total of students
if (students.length !== tokenInfo.total) {
throw new DataError('E_TOTAL_STUDENTS',
`${this.context.__("The number of students in the class differs from the generated tokens")}` +
`(${this.context.__("Students")}: ${students.length}, ${this.context.__("Number of tokens")}:${tokenInfo.total}.`);
}
// validate student emails, must all exist and have a valid format
// https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
const mailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
const invalidEmail = students.find(student => {
return !(student.email && mailRegex.test(student.email));
});
if (invalidEmail) {
throw new DataError('E_INVALID_EMAIL', `${context.__('At least one student\'s email is null or invalid')} (email:${invalidEmail.email || ' '} ${context.__('Student')}:${invalidEmail.student})`);
}
// get tokens
let tokens = await this.context.model('EvaluationAccessToken').where('evaluationEvent').equal(this.getId())
.select('access_token')
.silent()
.getAllItems();
// and map them to an array
tokens = tokens.map(token => token.access_token);
// get mail template
const mailTemplate = await context.model('MailConfiguration')
.where('target').equal('SendInstructorTokenAction')
.silent()
.getItem();
if (mailTemplate == null) {
throw new DataError('E_MISSING_TEMPLATE', 'Cannot execute send tokens action because template is missing for SendInstructorTokenAction.');
}
// check if there is already an active sendTokensAction
const alreadySendingTokens = await context.model('SendInstructorTokenAction')
.where('event').equal(event.id)
.and('actionStatus/alternateName').equal('ActiveActionStatus')
.silent().count();
if (alreadySendingTokens) {
throw new DataError('E_ALREADY_SENDING', `${context.__('The bulk sending process of tokens is already in progress')}`);
}
// save SendInstructorTokenAction
const sendTokenAction = {
event: this.getId(),
numberOfStudents: students.length,
total: tokenInfo.total,
sent: 0,
failed: 0,
actionStatus: {
alternateName: 'ActiveActionStatus'
}
}
sendAction = await context.model('SendInstructorTokenAction').silent().save(sendTokenAction);
// format mail subject expected params ${course}, ${instructor}
const exp = new RegExp('\\$\\{\\w+\\}+', 'ig');
subject = mailTemplate.subject;
const params = subject.match(exp);
if (params.length > 0) {
// parse only custom parameters ${instructor},${course}
params.forEach(param => {
if (param === '${instructor}') {
// get instructor full name
subject = subject.replace(param, eventInstructor ? `${eventInstructor.familyName} ${eventInstructor.givenName}` : '');
} else if (param === '${course}') {
// get courseClass code and title
subject = subject.replace(param, courseClass.course ? `${courseClass.course.displayCode} ${courseClass.title}` : '');
} else {
subject = subject.replace(param, '');
}
});
}
EvaluationEvent.sendMessages(context, students, tokens, tokenInfo, mailTemplate, subject, sendAction, event);
} catch (err) {
TraceUtils.error(err);
// create send tokens action
const sendTokenAction = {
event: this.getId(),
numberOfStudents: 0,
total: 0,
sent: 0,
failed: 0,
actionStatus: {
alternateName: 'FailedActionStatus'
},
description: err.message
}
// and save
if (sendAction && sendAction.id) {
sendTokenAction.id = sendAction.id;
}
if(!subject) {
// load courseClass
const item = await context.model('ClassInstructorEvaluationEvent').where('id').equal(this.getId())
.expand({
name: 'courseClassInstructor',
options: {
'$expand': 'courseClass($expand=course)'
}
})
.getItem();
const courseClass =item && item.courseClassInstructor && item.courseClassInstructor.courseClass
subject =courseClass? `${courseClass.course.displayCode} ${courseClass.title}`: '';
}
EvaluationEvent.sendSse(context.user,context, this.getId(), subject, err);
return await context.model('SendInstructorTokenAction').silent().save(sendTokenAction);
}
}
}
module.exports = ClassInstructorEvaluationEvent;