diff --git a/server/common/database/utils.js b/server/common/database/utils.js index d8bffd7..9d55982 100644 --- a/server/common/database/utils.js +++ b/server/common/database/utils.js @@ -22,7 +22,8 @@ const sqlFunctions = { max: 'MAX', average: 'AVG', count: 'COUNT', - distinct: 'DISTINCT' + distinct: 'DISTINCT', + sum: 'SUM' }; module.exports = { diff --git a/server/event/event.controller.js b/server/event/event.controller.js index 0590a91..d5a22d8 100644 --- a/server/event/event.controller.js +++ b/server/event/event.controller.js @@ -2,35 +2,28 @@ const db = require('../common/database'); const HttpStatus = require('http-status'); const map = require('lodash/map'); const pick = require('lodash/pick'); +const set = require('lodash/set'); const { GradedEvent, LearnerProfile, UngradedEvent, Sequelize, utils } = db; -const fn = utils.build(UngradedEvent); const { CREATED } = HttpStatus; const { Op } = Sequelize; const commonAttrs = ['userId', 'activityId', 'interactionStart', 'interactionEnd']; const ungradedAttrs = ['progress'].concat(commonAttrs); const gradedAttrs = ['questionId', 'isCorrect', 'answer'].concat(commonAttrs); - -const parseResult = it => ({ - ...it, - avgDuration: parseFloat(it.avgDuration), - views: parseInt(it.views, 10) -}); +const ungradedFilterAttrs = ['interactionStart', 'interactionEnd', 'activityIds']; +const gradedFilterAttrs = ungradedFilterAttrs.concat('questionIds'); function listUngradedEvents({ cohortId, query, options }, res) { - const { activityIds, uniqueViews, fromDate, toDate } = query; + const fn = utils.build(UngradedEvent); const group = [fn.column('activityId')]; - const views = uniqueViews ? fn.distinct('userId') : fn.column('userId'); + const views = query.uniqueViews ? fn.distinct('userId') : fn.column('userId'); const attributes = [ [...group, 'activityId'], [fn.count(views), 'views'], [fn.average('duration'), 'avgDuration'], [fn.max('interactionEnd'), 'lastViewed'] ]; - const where = { cohortId }; - if (fromDate) where.interactionStart = { [Op.gte]: fromDate }; - if (toDate) where.interactionEnd = { [Op.lte]: toDate }; - if (activityIds) where.activityId = { [Op.in]: activityIds }; + const where = getFilters({ cohortId, ...pick(query, ungradedFilterAttrs) }); const opts = { where, ...options, group, attributes, raw: true }; return UngradedEvent.findAndCountAll(opts).then(({ rows, count }) => { const items = map(rows, parseResult); @@ -38,6 +31,25 @@ function listUngradedEvents({ cohortId, query, options }, res) { }); } +function listGradedEvents({ cohortId, query, options }, res) { + const fn = utils.build(GradedEvent); + const group = [fn.column('questionId'), fn.column('activityId')]; + const attributes = [ + [fn.column('questionId'), 'questionId'], + [fn.column('activityId'), 'activityId'], + [fn.sum(Sequelize.cast(fn.column('isCorrect'), 'integer')), 'correct'], + [fn.count(fn.column('userId')), 'submissions'], + [fn.average('duration'), 'avgDuration'], + [fn.max('interactionEnd'), 'lastSubmitted'] + ]; + const where = getFilters({ cohortId, ...pick(query, gradedFilterAttrs) }); + const opts = { where, ...options, group, attributes, raw: true }; + return GradedEvent.findAndCountAll(opts).then(({ rows, count }) => { + const items = map(rows, parseResult); + return res.jsend.success(({ items, total: count.length })); + }); +} + async function reportUngradedEvent({ cohortId, body }, res) { const data = { cohortId, ...pick(body, ungradedAttrs) }; const profileCond = { cohortId, userId: body.userId }; @@ -54,13 +66,33 @@ async function reportGradedEvent({ cohortId, body }, res) { return res.status(CREATED).end(); } -function calculateDuration({ interactionStart, interactionEnd }) { - if (!interactionStart || !interactionEnd) return null; - return Math.ceil((interactionEnd - interactionStart) / 1000); -} - module.exports = { listUngradedEvents, + listGradedEvents, reportUngradedEvent, reportGradedEvent }; + +function parseResult(it) { + const intAttributes = ['views', 'submissions', 'correct']; + return Object.keys(it).reduce((acc, key) => { + if (key === 'avgDuration') return set(acc, key, parseFloat(it[key])); + if (intAttributes.includes(key)) return set(acc, key, parseInt(it[key], 10)); + return set(acc, key, it[key]); + }, {}); +} + +function getFilters(query) { + const { activityIds, cohortId, fromDate, toDate, questionIds } = query; + const where = { cohortId }; + if (fromDate) where.interactionStart = { [Op.gte]: fromDate }; + if (toDate) where.interactionEnd = { [Op.lte]: toDate }; + if (activityIds) where.activityId = { [Op.in]: activityIds }; + if (questionIds) where.questionId = { [Op.in]: questionIds }; + return where; +} + +function calculateDuration({ interactionStart, interactionEnd }) { + if (!interactionStart || !interactionEnd) return null; + return Math.ceil((interactionEnd - interactionStart) / 1000); +} diff --git a/server/event/index.js b/server/event/index.js index 2b4dc56..3b55e65 100644 --- a/server/event/index.js +++ b/server/event/index.js @@ -3,10 +3,11 @@ const ctrl = require('./event.controller'); const { processPagination } = require('../common/pagination'); const router = require('express').Router(); -const { UngradedEvent } = require('../common/database'); +const { GradedEvent, UngradedEvent } = require('../common/database'); router .get('/ungraded', processPagination(UngradedEvent), ctrl.listUngradedEvents) + .get('/graded', processPagination(GradedEvent), ctrl.listGradedEvents) .post('/ungraded', ctrl.reportUngradedEvent) .post('/graded', ctrl.reportGradedEvent);