UNPKG

@universis/evaluations

Version:

Universis evaluations library

341 lines (328 loc) 16.7 kB
import {EdmMapping, EdmType} from '@themost/data'; import EvaluationEvent from "./evaluation-event-model"; import {DataError, TraceUtils, DataNotFoundError, LangUtils} from "@themost/common"; import moment from "moment"; /** * @class */ @EdmMapping.entityType('CourseClassEvaluationEvent') class CourseClassEvaluationEvent extends EvaluationEvent { /** * @constructor */ constructor() { super(); } @EdmMapping.func('Results', EdmType.CollectionOf('Object')) async getResults() { // check access to ClassInstructorEvaluationEvent and get results silently const event = await this.context.model('CourseClassEvaluationEvent').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(); } } @EdmMapping.action('GenerateEvaluationTokens', 'GenerateCourseClassTokenAction') async generateEvaluationTokens() { const context = this.context; const action = { event: this.id, numberOfStudents: 0, total: 0, actionStatus: {alternateName: 'PotentialActionStatus'} }; // get courseClass event const event = await context.model('CourseClassEvaluationEvent').where('id').equal(this.id) .expand('courseClass') .getItem(); if (event) { //check if a GenerateCourseClassTokenAction exists const prevAction = await context.model('GenerateCourseClassTokenAction').where('event').equal(this.id) .and('actionStatus/alternateName').notEqual('FailedActionStatus') .silent().getItem(); if (prevAction) { throw new Error(context.__('A previous action for generating evaluation tokens has been found.')); } const courseClass = event.courseClass; action.numberOfStudents = await context.model('StudentCourseClass') .where('courseClass').equal(courseClass.id).silent().count(); // save action and then generateTokens const generateAction = await context.model('GenerateCourseClassTokenAction').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('CourseClassEvaluationEvent').save(event); if (event.numberOfStudents > 0) { const studentMetadata = context.getConfiguration().getSourceAt('settings/evaluation/useStudentInfoAtMetadata') || false; await EvaluationEvent.generateTokens(context, generateOptions); // get all generated tokens and generate tokens for child events const tokens = await context.model('EvaluationAccessToken') .where('evaluationEvent').equal(event.id).silent().getAllItems(); const students = await this.getStudentEvaluationEvents(context, event, courseClass); for (const student of students) { const access_token = tokens.pop(); // student.id const metadata = { id: studentMetadata ? student.id: null, student: studentMetadata ? student.student : null, semester: student.semester, registrationType: student.registrationType }; for (let i = 0; i < student.events.length; i++) { const studentEvent = student.events[i]; if (event.id === studentEvent.id) { await context.model('EvaluationAccessToken').silent().save({ access_token: access_token.access_token, metadata }); } else { await context.model('EvaluationAccessToken').silent().save({ evaluationEvent: studentEvent, expires: generateOptions.expires, parentToken: access_token, metadata }); } } } } generateAction.total = action.numberOfStudents; generateAction.actionStatus = {alternateName: 'CompletedActionStatus'} return await context.model('GenerateCourseClassTokenAction').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('GenerateCourseClassTokenAction').save(generateAction); } } } @EdmMapping.func('TokenInfo', EdmType.CollectionOf('Object')) async getTokenStatistics() { const tokenInfo = { total: 0, used: 0, sent: 0 }; // get number of 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; } @EdmMapping.action('SendEvaluationTokens', 'SendCourseClassTokenAction') 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('CourseClassEvaluationEvent').where('id').equal(this.getId()) .and('date(endDate)').greaterOrEqual(currentDate) .expand({ name: 'courseClass', options: { '$expand': 'course' } }, { 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 const courseClass = event.courseClass; const students = await this.getStudentEvaluationEvents(context,event,courseClass); 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()) .silent() .getAllItems(); // get mail template const mailTemplate = await context.model('MailConfiguration') .where('target').equal('SendCourseClassTokenAction') .silent() .getItem(); if (mailTemplate == null) { throw new DataError('E_MISSING_TEMPLATE', 'Cannot execute send tokens action because template is missing for SendCourseClassTokenAction.'); } // check if there is already an active sendTokensAction const alreadySendingTokens = await context.model('SendCourseClassTokenAction') .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('SendCourseClassTokenAction').silent().save(sendTokenAction); // format mail subject expected params ${course} const exp = new RegExp('\\$\\{\\w+\\}+', 'ig'); subject = mailTemplate.subject; const params = subject.match(exp); if (params && params.length > 0) { // parse only custom parameters ${instructor},${course} params.forEach(param => { 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('CourseClassEvaluationEvent').where('id').equal(this.getId()) .expand({ name: 'courseClass', options: { '$expand': 'course' } }) .getItem(); const courseClass =item && item.courseClass subject =courseClass? `${courseClass.course.displayCode} ${courseClass.title}`: ''; } EvaluationEvent.sendSse(context.user,context, this.getId(), subject, err); return await context.model('SendCourseClassTokenAction').silent().save(sendTokenAction); } } async getStudentEvaluationEvents(context, event, courseClass) { // get child events (classInstructorEvaluationEvents) const classInstructorEvaluationEvents = await context.model('ClassInstructorEvaluationEvents') .where('superEvent') .equal(event.id) .expand({ name: 'courseClassInstructor', options: { '$expand': 'courseClass($expand=sections)' } }) .silent() .getAllItems(); // get course class sections' instructors const instructorSections = await context.model('CourseClassSectionInstructor').where('courseClass').equal(courseClass.id) .select('section/section as section', 'instructor').flatten().silent().getAllItems(); const students = await context.model('StudentCourseClass').where('courseClass') .equal(courseClass.id).silent().getAllItems(); for (const student of students) { student.events = []; student.events.push(event); if (classInstructorEvaluationEvents && classInstructorEvaluationEvents.length > 0) { let instructorEvents = []; if (instructorSections && instructorSections.length && LangUtils.parseInt(courseClass.mustRegisterSection)===1) { instructorEvents = classInstructorEvaluationEvents.filter(x => { return x.courseClassInstructor && instructorSections.filter(x => { return student.section === x.section }).map(x => { return x.instructor; }).includes(x.courseClassInstructor.instructor) }); } if (instructorEvents && instructorEvents.length) { instructorEvents.forEach(x => { student.events.push(x); }) } else { student.events.push.apply(student.events, classInstructorEvaluationEvents); } } } return students; } } module.exports = CourseClassEvaluationEvent;