UNPKG

@universis/evaluations

Version:

Universis evaluations library

966 lines (931 loc) 43.7 kB
import { HttpNotFoundError, HttpTokenExpiredError, HttpServerError, TraceUtils, HttpBadRequestError, Args, LangUtils, DataError, DataNotFoundError } from '@themost/common'; import { DataConfigurationStrategy, DataContext, DataObject, EdmMapping, EdmType, ODataModelBuilder } from '@themost/data'; import { HttpContext } from '@themost/web'; import { JsonFormInspector } from '@themost/form-inspector'; import EvaluationDocument from './evaluation-document-model'; const EvaluationAccessToken = require('./evaluation-access-token-model'); import { lt, neq, valid, coerce } from 'semver'; import util from "util"; import fs from "fs"; import moment from "moment"; import {getMailer} from "@themost/mailer"; import {EvaluationService} from "../EvaluationService"; const math = require('mathjs'); /** * @class */ @EdmMapping.entityType('EvaluationEvent') class EvaluationEvent extends DataObject { /** * @constructor */ constructor() { super(); } /** * Get current evaluation based on the evaluation key contained in Evaluation-Key HTTP header * @param {HttpContext} context */ @EdmMapping.func('Current', 'EvaluationEvent') static async getCurrent(context) { // get evaluation key const token = await EvaluationAccessToken.inspect(context, context.user && context.user.authenticationToken); if (token.active === false) { throw new HttpTokenExpiredError(context.__('Evaluation access token has been expired or is invalid.')); } // get evaluation event // get current date let evaluationEvent = await context.model('EvaluationEvent') .where('id').equal(token.sub) .expand('eventStatus','subEvents') .silent().getTypedItem(); if (evaluationEvent == null) { throw new HttpNotFoundError('The specified evaluation cannot be found'); } if (evaluationEvent.eventStatus ) { if (evaluationEvent.eventStatus.alternateName!=='EventOpened') { throw new DataError('E_CLOSED_EVENT',context.__('The course evaluation has been closed and is no longer available') ); } } // check dates const currentDate = new Date().setHours(0,0,0,0); const startDate = evaluationEvent.startDate.setHours(0,0,0,0); const endDate = evaluationEvent.endDate.setHours(0,0,0,0); if (currentDate < startDate) { throw new DataError('E_NOT_STARTED',context.__('Evaluation period for this course has not been started yet')); } if (currentDate > endDate) { throw new DataError('E_EXPIRED',context.__('Evaluation period for this course has been expired')); } let courseClass; let courseClassTitle; switch (evaluationEvent.additionalType) { case 'ClassInstructorEvaluationEvent': const event = await context.model('ClassInstructorEvaluationEvent') .where('id').equal(token.sub) .expand({ name: 'courseClassInstructor', options: { $expand: 'courseClass($expand=course,year,period,locale),instructor' } }) .silent().getTypedItem(); courseClass = event.courseClassInstructor.courseClass; const instructor = event.courseClassInstructor.instructor; courseClassTitle = courseClass.locale && courseClass.locale.title || courseClass.title; evaluationEvent.courseClass = `${courseClass.course.displayCode}-${courseClassTitle} (${courseClass.year.alternateName}-${courseClass.period.name}) `; evaluationEvent.instructor = `${instructor.familyName} ${instructor.givenName}`; break; case 'CourseClassEvaluationEvent': const courseEvent = await context.model('CourseClassEvaluationEvent') .where('id').equal(token.sub) .expand({ name: 'courseClass', options: { $expand: 'course,year,period,locale,instructors($expand=instructor)' } }) .silent().getTypedItem(); courseClass = courseEvent.courseClass; courseClassTitle = courseClass.locale && courseClass.locale.title || courseClass.title; evaluationEvent.courseClass = `${courseClass.course.displayCode}-${courseClassTitle} (${courseClass.year.alternateName}-${courseClass.period.name}) `; // check if child evaluation tokens const instructorTokens = await context.model('EvaluationAccessToken').where('parentToken') .equal(token.access_token).silent().getAllItems(); if (instructorTokens && instructorTokens.length>0) { evaluationEvent.subEvents = []; for (let i = 0; i < instructorTokens.length; i++) { const instructorToken = await EvaluationAccessToken.inspect(context, instructorTokens[i].access_token); if (instructorToken.active) { // load evaluation event for getting instructor name const subEvent = await context.model('ClassInstructorEvaluationEvent') .where('id').equal(instructorTokens[i].evaluationEvent) .and('superEvent').equal(evaluationEvent.id) .and('eventStatus/alternateName').equal('EventOpened') .select('id', 'courseClassInstructor/instructor/familyName as familyName', 'courseClassInstructor/instructor/givenName as givenName') .silent().getItem(); if (subEvent) { subEvent.instructor = `${subEvent.familyName} ${subEvent.givenName}`; subEvent.access_token = instructorTokens[i].access_token; evaluationEvent.subEvents.push(subEvent); } } } } break; case 'default': evaluationEvent = await evaluationEvent.silent().getAdditionalObject(); break; } return evaluationEvent; } @EdmMapping.func('form', 'Object') async getForm() { const form = await this.property('evaluationDocument').getItem(); const service = this.context.getApplication().getService(function PrivateContentService(){}); /** * @type {string} */ const formPath = await new Promise((resolve, reject) => { service.resolvePhysicalPath(this.context, form, (err, physicalPath) => { if (err) { return reject(err); } return resolve(physicalPath); }) }); const readFileAsync = util.promisify(fs.readFile); let content = await readFileAsync(formPath); if (content == null) { throw new HttpNotFoundError('Evaluation form cannot be found'); } let buffer = Buffer.from(content,'base64'); let result; result = JSON.parse(buffer.toString('utf8')); try { result.properties = result.properties || {}; Object.assign(result.properties, { name: form.resultType, _id: form.id, _event: this.id }); // post-processing let shouldUpgrade = true; let model = this.context.model(form.resultType); if (model != null) { // try to convert model and form versions to a valid semver format const modelVersion = valid(coerce(model.version || '1.0.0')); const formVersion = valid(coerce(form.version || '1.0.0')); // and upgrade evaluation document only if its form has been upgraded shouldUpgrade = lt(modelVersion, formVersion); } if (shouldUpgrade) { // load for inspector const inspector = new JsonFormInspector(); model = inspector.inspect(result); // post processing steps Object.assign(model, { source: form.resultType, // add source from evaluation document e.g. Evaluation1 view: form.resultType, // add view from evaluation document e.g. Evaluation1 version: model.version ,// set version (follow updates) hidden: true }); // add evaluation event and evaluation document associations // for further processing of evaluation results model.fields.push.apply(model.fields, [ { name: 'evaluationEvent', type: this.getModel().name, editable: false, nullable: false, indexed: true, } ]); // reset privileges /** * @type {DataConfigurationStrategy|*} */ const dataConfiguration = this.context.getApplication() .getConfiguration().getStrategy(DataConfigurationStrategy); model.privileges.splice(0); // get resultPrivileges const thisModel = dataConfiguration.model(this.getModel().name); model.privileges.push.apply(model.privileges, [ { "mask": 15, "type": "global" }, { "mask": 1, "type": "global", "account": "Administrators" } ]); // add extra privileges if (Array.isArray(thisModel.resultPrivileges)) { model.privileges.push.apply(model.privileges, thisModel.resultPrivileges); } dataConfiguration.setModelDefinition(model); // add entity set const builder = this.context.getApplication().getService(ODataModelBuilder); builder.addEntitySet(model.name, model.name); if (model.hidden) { builder.removeEntitySet(model.name); } // try to convert model and form versions to a valid semver format const modelVersion = valid(coerce(model.version || '1.0.0')); const formVersion = valid(coerce(form.version || '1.0.0')); // and if they are different if (neq(modelVersion, formVersion)) { // update evaluation document version await this.context.model('EvaluationDocument').silent().save({ id: form.id, // note: save semver version to satisfy the pattern of the field // see "pattern": "^\\d+.\\d+.\\d+$" version: modelVersion }); } } // check if event has sub events and append form to result if (this.subEvents && this.subEvents.length) { // get first subEvent for const subEvent = this.context.model('EvaluationEvent').convert(this.subEvents[0]); const subResult = await subEvent.getForm(); // add select boxes for each instructor if (this.subEvents.length>1) { const label = this.context.__('Select at least one instructor to evaluate') const selectInstructorsComponent = { "label": label, "optionsLabelPosition": "right", "tableView": false, "key": "instructors", "type": "selectboxes", "input": true, "inputType": "checkbox", "validate": { "required": true, } }; this.subEvents.sort((a, b) => (a.instructor < b.instructor) ? -1 : 1); selectInstructorsComponent.values = this.subEvents .map(x => { x.label = x.instructor; x.value= x.id ; return x; }); result.components.push(selectInstructorsComponent); const headerComponent = subResult.components[0]; headerComponent.customConditional = "show =(data && data.instructors[row.id])"; headerComponent.refreshOn= "instructors"; } // create panel const repeaterComponent = { "label": " ", "disableAddingRemovingRows": true, "reorder": false, "customClass": "border-0", "addAnotherPosition": "bottom", "layoutFixed": false, "enableRowGroups": false, "initEmpty": false, "hideLabel": true, "tableView": false, "key": "subEvents", "type": "datagrid", "input": true, "components": [] }; repeaterComponent.components.push.apply(repeaterComponent.components, subResult.components); result.components.push(repeaterComponent); } return result; } catch (err) { TraceUtils.error(`Error while decoding evaluation form ${form.id} content.`); TraceUtils.error(err); throw new HttpServerError('An error occurred while decoding evaluation form.'); } } @EdmMapping.func('Results', EdmType.CollectionOf('Object')) async getResults() { const form = await this.getForm(); return this.context.model(form.properties.name).where('evaluationEvent').equal(this.id).prepare(); } @EdmMapping.func('Tokens', EdmType.CollectionOf('EvaluationAccessToken')) async getAccessTokens() { return this.context.model('EvaluationAccessToken').where('evaluationEvent').equal(this.id).prepare(); } @EdmMapping.func('FormAttributes', EdmType.CollectionOf('Object')) async getFormAttributes() { /** * @type {DataContext|*} */ const context = this.context; const results = await this.getFormResults (context,this.getId(),null); return results.responses; } @EdmMapping.func('FormAttributesSummary', EdmType.CollectionOf('Object')) async getFormAttributesSummary() { /** * @type {DataContext|*} */ const context = this.context; const eventModel = this.getModel().name; const results = await this.getFormResults(context, this.getId(), null); let q = this.context.model(eventModel).where('id').equal(this.getId()).expand({ 'name': 'tokens', 'options': { '$select': 'evaluationEvent,count(evaluationEvent) as total,used', '$groupby': 'evaluationEvent,used' }},'organizer'); if (eventModel==='ClassInstructorEvaluationEvent') { q = this.context.model(eventModel).where('id').equal(this.getId()).expand({ 'name': 'tokens', 'options': { '$select': 'evaluationEvent,count(evaluationEvent) as total,used', '$groupby': 'evaluationEvent,used' } } , { 'name': 'courseClassInstructor', 'options': { '$expand': 'courseClass($expand=year, period, course($expand=department($expand=locale), locale), locale), instructor' } }); } // check if target event model has a courseClass field const courseClassField = (this.context.model(eventModel).fields || []) .find((field) => field.type === 'CourseClass'); if (courseClassField) { // and expand it q.expand({ name: courseClassField.name, options: { $expand: 'year, period, course($expand=department($expand=locale), locale), locale' } }); } results.event = await q.getItem(); if (results.event==null) { throw new DataNotFoundError('Evaluation event not found.'); } // count used tokens of event if (results.event.tokens) { const usedTokens = results.event.tokens && results.event.tokens.find(x => { return x.used === true; }); results.event.tokensUsed = usedTokens ? usedTokens.total : 0; } // get avg of questions avg const avg = this.getAvgValues(context, results); results.event.responsesAvg = avg.responsesAvg; results.event.questionsAvg = avg.questionsAvg; return results; } async getFormResults (context, id, events) { const result = []; const evaluationEvents = []; if (id) {evaluationEvents.push(id);} else { if (Array.isArray(events) && events.length) { evaluationEvents.push.apply(evaluationEvents, events.map(x => { return x.id || x; })); } } if (evaluationEvents.length===0) { throw new DataError('Evaluation events not supplied.'); } const form = await this.getForm(); this.setParentLegend(form); // get model attributes /** * @type {DataConfigurationStrategy|*} */ const dataConfiguration = context.getApplication() .getConfiguration().getStrategy(DataConfigurationStrategy); const dataTypes = dataConfiguration.dataTypes; const q = context.model(form.properties.name).asQueryable(); const selectArguments = []; const model = dataConfiguration.model(form.properties.name); const attributes = model.fields; let totalResponses = 0; const summaryValues = []; const currentLocale = context.locale; const settings = form.settings && form.settings.i18n; const translations = settings && settings[currentLocale] || {}; attributes.forEach(attribute => { const key = this.findByKey(form.components, attribute.name); if (key) { attribute = Object.assign(attribute, { name: attribute.name, label: key.label }); attribute.isNumber = false; attribute.legend = key.customProperties && key.customProperties.legend; if (Object.prototype.hasOwnProperty.call(dataTypes, attribute.type)) { attribute.isNumber = dataTypes[attribute.type].type === 'number'; } selectArguments.push(`${attribute.name}`); // clear std,avg and responses from model attribute attribute.std = 0; attribute.avg = 0; delete attribute.responses; // prepare a values mapper const valuesMapper = (keyValue) => { return { value: keyValue.value, // use localized label, if it exists label: translations[keyValue.label] || keyValue.label } }; if (key.type === 'radio') { if (attribute.isNumber) { if (summaryValues.length === 0) { // add radio values from first key summaryValues.push.apply(summaryValues, key.values); } else { //add values only if not exist summaryValues.push.apply(summaryValues, key.values.filter(x => { return summaryValues.findIndex(y => { return y.value === x.value }) < 0; })); } } else { // prepare the attribute's values for post-processing (e.g. count) attribute.values = (key.values || []).map(valuesMapper); } } if (key.type === 'selectboxes' && !attribute.isNumber) { // prepare the attribute's values for post-processing (e.g. count) attribute.values = (key.values || []).map(valuesMapper); } // clone attribute let attr = { ...attribute }; attr.legend = translations[attr.legend] || attr.legend; attr.title = translations[attr.title] || attr.title; attr.label = translations[attr.label] || attr.label; result.push(attr); } }); // add total = 0 to summaryValues summaryValues.map(x => { x.total = 0; return x; }); const showResults = await this.showResults(context); // check if zero values should be excluded const ignoreZeroValues = context.getConfiguration().getSourceAt('settings/evaluation/ignoreZeroValues') || false; if (selectArguments.length && showResults === true) { const responses = await q.select.apply(q, selectArguments).where('evaluationEvent').in(evaluationEvents).silent().getAllItems(); for (let i = 0; i < result.length; i++) { const qElement = result[i]; qElement.responses=[]; if (qElement.isNumber) { // remove zero values from responses avg and std const sample = responses.filter(x=> { if (ignoreZeroValues) { return x[qElement.name] != 0; } return true; }); // add mean value and standard deviation qElement.std = sample.length? math.std(sample .map(x => { return x[qElement.name]; })):0; qElement.avg = sample.length? math.mean(sample .map(x => { return x[qElement.name]; })):0; qElement.numberOfResponses = sample.length; summaryValues.forEach(radioValue => { const result = responses.filter(y => { return y[qElement.name] == radioValue.value; }).length; radioValue.total += result; totalResponses += result; }); qElement.responses = responses.map(x => { if ((x[qElement.name]>0 && ignoreZeroValues) || !ignoreZeroValues) { return x[qElement.name]; } }); } else { qElement.responses = responses.map(x => { const value = x[qElement.name]; // if the field type is Json if (qElement.type === 'Json') { // enumerate value keys and consider only truthy values return Object.keys(value).filter((item) => { return !!value[item]; }); } // else, just return the value return value; }); // check if the element contains values (post-processing) if (Array.isArray(qElement.values) && qElement.values.length > 0) { // enumerate them qElement.values = qElement.values.map((keyValue) => { // and append the count keyValue.count = qElement.responses.filter((qResponse) => { // if the field's type is Json if (qElement.type === 'Json') { // count responses which include the item return (qResponse || []).includes(keyValue.value); } // else, just count flat items return keyValue.value === qResponse; }).length; return keyValue; }); } } } } const results={}; results.responses = result; results.summaryValues = []; results.summaryValues.push.apply(results.summaryValues, summaryValues.map(x=>{ x.totalResponses= totalResponses; return x; })); return results; } async showResults (context) { // this is a custom way to get permissions based on eventStatus and user groups let showResults = true; let found = false; const eventStatusesResults = context.getConfiguration().getSourceAt('settings/evaluation/results/statuses') || []; const groupsAllowed = context.getConfiguration().getSourceAt('settings/evaluation/results/groupsAllowed') || []; if (eventStatusesResults.length > 0) { // check if eventStatus is allowed if (groupsAllowed.length > 0) { // but first check if user belongs to allowed groups const userName = context.interactiveUser ? context.interactiveUser.name : context.user.name; const user = await context.model('User').where('name').equal(userName).expand('groups').getItem(); found = user.groups.some(r => groupsAllowed.indexOf(r.name) >= 0); } if (!found) { const evaluationEvent = await context.model('EvaluationEvent').where('id').equal(this.getId()) .select('id', 'eventStatus/alternateName as eventStatus') .getItem(); if (evaluationEvent == null) { throw new HttpNotFoundError('The specified evaluation cannot be found'); } showResults = eventStatusesResults.findIndex(x => { return x === evaluationEvent.eventStatus; }) >= 0; } } return showResults; } findByKey(array, key) { for (let index = 0; index < array.length; index++) { const element = array[index]; if (element.key === key) { return element; } else { if (element.components) { const found = this.findByKey(element.components, key); if (found) { return found; } } } } } setParentLegend(component) { if (Array.isArray(component.components)) { component.components.forEach(item => { if (component.legend) { item.customProperties = item.customProperties || {}; item.customProperties.legend = component.legend; } this.setParentLegend(item); }); } } @EdmMapping.param('data', 'Object', false, true) @EdmMapping.action('Evaluate', 'Object') async evaluate(data) { const token = await EvaluationAccessToken.inspect(this.context, this.context.user.authenticationToken); if (token.active === false) { throw new HttpTokenExpiredError('Evaluation access token has been expired or is invalid.'); } if (this.id !== token.sub) { throw new HttpBadRequestError('Evaluation access token is invalid'); } // get evaluation form const form = await this.getForm(); // get evaluation document if (form == null) { throw new HttpNotFoundError('Evaluation form cannot be found'); } // get evaluation document const evaluationDocument = await this.context.model(EvaluationDocument) .where('id').equal(form.properties._id) .silent().getItem(); if (evaluationDocument == null) { throw new HttpNotFoundError('Evaluation document cannot be found'); } // assign defaults Object.assign(data, { evaluationEvent: this.id, evaluationDocument: evaluationDocument.id }); // and finally append evaluation await this.context.model(evaluationDocument.resultType).on('after.save', (event, callback) => { const context = event.model.context; context.model(EvaluationAccessToken).where('access_token').equal(context.user && context.user.authenticationToken) .silent().getItem().then(async (token) => { if (token == null) { throw new HttpTokenExpiredError('Evaluation token is required'); } // check if subEvents have been evaluated const subEvents = event.target.subEvents; if (subEvents && subEvents.length) { // evaluate each sub event for (let i = 0; i < subEvents.length; i++) { context.user.authenticationToken = subEvents[i].access_token; const subEvent = context.model('EvaluationEvent').convert(subEvents[i].id); delete subEvents[i].id; if (subEvents.length>1 && event.target.instructors) { // check if instructor has been evaluated if (event.target.instructors[subEvent.id] === true) { await subEvent.evaluate(subEvents[i]); } } else { await subEvent.evaluate(subEvents[i]); } } } return context.model(EvaluationAccessToken).silent().save({ access_token: token.access_token, expires: new Date(0), // set epoch date used: true }); }).then(() => { return callback(); }).catch((err) => { TraceUtils.error('An error occurred while submitting evaluation'); return callback(err); }); }).silent().save(data); return data; } static async generateTokens(context, generateOptions) { let items = []; if (Array.isArray(generateOptions)) { let n = 0; let newItems; // eslint-disable-next-line no-unused-vars for(let itemGenerateOptions of generateOptions) { newItems = await EvaluationEvent.generateManyTokens(context, itemGenerateOptions); n += newItems.length; items.push.apply(items, newItems); } await context.model('EvaluationAccessToken').silent().save(items); return n } else { items = await EvaluationEvent.generateManyTokens(context, generateOptions); await context.model('EvaluationAccessToken').silent().save(items); return items.length; } } /** * Generates a number of access tokens for the given event * @param {DataContext} context * @param {{evaluationEvent: number, n: number, expires?: DateTime}} generateOptions */ static async generateManyTokens(context, generateOptions) { const n = generateOptions.n; Args.check(typeof n === 'number', 'Expected a number greater than zero'); Args.check(n > 0, 'The number of access tokens must be greater than zero'); let maximumGenerateTokens = LangUtils.parseInt(context.getConfiguration().getSourceAt('settings/evaluation/maximumGenerateTokens')); if (maximumGenerateTokens === 0) { maximumGenerateTokens = 1000; } Args.check(n < maximumGenerateTokens, `The specified number of tokens exceeded the maximum allowed (${maximumGenerateTokens})`); const items = []; // generate tokens for (let index = 0; index < n; index++) { items.push({ evaluationEvent: generateOptions.evaluationEvent, expires: generateOptions.expires }); } return items; } /** * * @param {import('@themost/express').ExpressDataContext} appContext * @param students * @param tokens * @param tokenInfo * @param mailTemplate * @param subject * @param action * @param evaluationEvent */ static sendMessages(appContext, students, tokens, tokenInfo, mailTemplate, subject, action, evaluationEvent) { const app = appContext.getApplication(); const context = app.createContext(); const user= appContext.user; context.user = user; let failed = []; let succeeded = []; let message = subject; const transporter= appContext.getApplication().getService(EvaluationService).getMailTransporter(); // use an async function to send emails (async () => { for (const student of students) { // refresh mailer const mailer = getMailer(context); // maybe investigate why this has to be refreshed. // pop an access token let access_token; const studentMetadata = context.getConfiguration().getSourceAt('settings/evaluation/useStudentInfoAtMetadata') || false; // student metadata contains student identifiers only if setting useStudentInfoAtMetadata is true // otherwise only semester and registrationType are stored const metadata = { id: studentMetadata ? student.id: null, student: studentMetadata ? student.student : null, semester: student.semester, registrationType: student.registrationType }; if (studentMetadata) { // try to find access token for specific student access_token = tokens.find(x=>{ return x.metadata && x.metadata.student === student.id; }); } if (!access_token) { access_token = tokens.pop(); // update metadata } else { // TraceUtils.log('test') } access_token.metadata = metadata; // send message await new Promise(resolve => { mailer.transporter(transporter) .template(mailTemplate.template) .subject(subject) .to(student.email) .send(Object.assign(evaluationEvent, { access_token: access_token.access_token }), (err) => { if (err) { try { TraceUtils.error(`SendEvaluationTokensAction', 'An error occurred while trying to send token to student email ${student.email} for evaluation event ${evaluationEvent.id}`); TraceUtils.error(err); } catch (err1) { // do nothing } failed.push(access_token); return resolve(); } succeeded.push(access_token); // update successfully sent token return context.model('EvaluationAccessToken').silent().save({ access_token: access_token.access_token, // access_token is primary sent: true, metadata:access_token.metadata, $state: 2 }).then(χ => { return resolve(); }).catch(err => { TraceUtils.error(err); return resolve(); }); }); }); } })().then(() => { context.finalize(() => { // create send tokens action const sendTokensAction = { id: action && action.id || null, event: evaluationEvent, numberOfStudents: students.length, total: tokenInfo.total, sent: succeeded.length, failed: failed.length, actionStatus: { alternateName: tokenInfo.total === failed.length ? 'FailedActionStatus' : 'CompletedActionStatus' }, description: tokenInfo.total === failed.length ? context.__('Sending tokens failed for all students') : null } // and save return context.model(action.additionalType).silent().save(sendTokensAction).then(item => { EvaluationEvent.sendSse(user,context,evaluationEvent.id,message, sendTokensAction.description?{message: sendTokensAction.description}:null); }).catch(err => { TraceUtils.error(err); }); }); }).catch(err => { context.finalize(() => { // log error TraceUtils.error(err); // update successfully sent tokens const succeededTokens = succeeded.map(succeededToken => { return { access_token: succeededToken, sent: true, $state: 2 } }) return context.model('EvaluationAccessToken').silent().save(succeededTokens).then(evaluationTokens => { // create send tokens action const sendTokensAction = { id: action && action.id || null, event: evaluationEvent, numberOfStudents: students.length, total: tokenInfo.total, sent: succeeded.length, failed: failed.length, actionStatus: { alternateName: tokenInfo.total === failed.length ? 'FailedActionStatus' : 'CompletedActionStatus' }, description: tokenInfo.total === failed.length ? context.__('Sending tokens failed for all students') : null } // and save return context.model(action.additionalType).silent().save(sendTokensAction).then(tokenAction => { EvaluationEvent.sendSse(user,context,evaluationEvent.id,message,err); }).catch(err => { TraceUtils.error(err); }); }); }); }); } static sendSse(user, context, evaluationEvent, title, error) { // try to find ServerSentEventService const sse = context.getApplication().getService(function ServerSentEventService() {}); // only if it exists - it is not required if (sse != null) { if (user != null) { try { // create an event const event = { entitySet: 'EvaluationEvents', entityType: "EvaluationEvent", target: { id: evaluationEvent, message: error? `${title} ${error.message}`: title }, status: { success: !error , }, dateCreated: new Date() } // and send it to the user that is performing the approval (registrar scope) sse.emit(user.name, user.authenticationScope || "qa", event); } catch (err){ TraceUtils.error(`An error occurred while trying to send sse event`); TraceUtils.error(err); } } } } getAvgValues (context, results) { const avg = { responsesAvg:0, questionsAvg:0 } // get avg of questions avg const ignoreZeroValues = context.getConfiguration().getSourceAt('settings/evaluation/ignoreZeroValues') || false; // filter only numbers const filteredResults = results.responses.filter(x=> { return !!x.isNumber; }); // filter avg values const filteredByValueResults =filteredResults .filter(x=>{ return (ignoreZeroValues && x.avg > 0 || !ignoreZeroValues); }); avg.responsesAvg = filteredByValueResults.length ? math.mean(filteredByValueResults .map(x => { return x.avg ; })) : 0; // get all answers const questionResults = filteredResults.length? filteredResults.reduce((prev, curr)=>{ if (curr.responses) { prev.push(...curr.responses.filter(y => { return y !== undefined; }).map(y => { return y; })); } return prev; }, []):[]; avg.questionsAvg = questionResults.length? math.mean(questionResults):0; return avg; } } module.exports = EvaluationEvent;