@universis/evaluations
Version:
Universis evaluations library
966 lines (931 loc) • 43.7 kB
JavaScript
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
*/
.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
*/
.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;
}
.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.');
}
}
.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();
}
.func('Tokens', EdmType.CollectionOf('EvaluationAccessToken'))
async getAccessTokens() {
return this.context.model('EvaluationAccessToken').where('evaluationEvent').equal(this.id).prepare();
}
.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;
}
.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);
});
}
}
.param('data', 'Object', false, true)
.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;