From 701fccc62bf27327650cb840441ba11ca4ef90fb Mon Sep 17 00:00:00 2001 From: franosincic Date: Wed, 5 Jun 2019 18:49:29 +0200 Subject: [PATCH 1/3] List Graded Events API endpoint --- server/common/database/utils.js | 3 ++- server/event/event.controller.js | 33 +++++++++++++++++++++++++++++++- server/event/index.js | 3 ++- 3 files changed, 36 insertions(+), 3 deletions(-) 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..189e252 100644 --- a/server/event/event.controller.js +++ b/server/event/event.controller.js @@ -4,7 +4,6 @@ const map = require('lodash/map'); const pick = require('lodash/pick'); const { GradedEvent, LearnerProfile, UngradedEvent, Sequelize, utils } = db; -const fn = utils.build(UngradedEvent); const { CREATED } = HttpStatus; const { Op } = Sequelize; const commonAttrs = ['userId', 'activityId', 'interactionStart', 'interactionEnd']; @@ -17,7 +16,15 @@ const parseResult = it => ({ views: parseInt(it.views, 10) }); +const parseGradedResult = it => ({ + ...it, + avgDuration: parseFloat(it.avgDuration), + submissions: parseInt(it.submissions, 10), + correct: parseInt(it.correct, 10) +}); + function listUngradedEvents({ cohortId, query, options }, res) { + const fn = utils.build(UngradedEvent); const { activityIds, uniqueViews, fromDate, toDate } = query; const group = [fn.column('activityId')]; const views = uniqueViews ? fn.distinct('userId') : fn.column('userId'); @@ -38,6 +45,29 @@ function listUngradedEvents({ cohortId, query, options }, res) { }); } +function listGradedEvents({ cohortId, query, options }, res) { + const fn = utils.build(GradedEvent); + const { activityIds, fromDate, toDate } = query; + 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.distinct('userId')), 'submissions'], + [fn.average('duration'), 'avgDuration'], + [fn.max('interactionEnd'), 'lastSubmitted'] + ]; + 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 opts = { where, ...options, group, attributes, raw: true }; + return GradedEvent.findAndCountAll(opts).then(({ rows, count }) => { + const items = map(rows, parseGradedResult); + 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 }; @@ -61,6 +91,7 @@ function calculateDuration({ interactionStart, interactionEnd }) { module.exports = { listUngradedEvents, + listGradedEvents, reportUngradedEvent, reportGradedEvent }; 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); From 32ed1610e84fee77d1adfad0318eb452b360f9c2 Mon Sep 17 00:00:00 2001 From: franosincic Date: Wed, 5 Jun 2019 19:23:10 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Cleanup=20=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/event/event.controller.js | 56 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/server/event/event.controller.js b/server/event/event.controller.js index 189e252..82a359b 100644 --- a/server/event/event.controller.js +++ b/server/event/event.controller.js @@ -1,4 +1,5 @@ const db = require('../common/database'); +const has = require('lodash/has'); const HttpStatus = require('http-status'); const map = require('lodash/map'); const pick = require('lodash/pick'); @@ -9,35 +10,38 @@ const { Op } = Sequelize; const commonAttrs = ['userId', 'activityId', 'interactionStart', 'interactionEnd']; const ungradedAttrs = ['progress'].concat(commonAttrs); const gradedAttrs = ['questionId', 'isCorrect', 'answer'].concat(commonAttrs); +const ungradedQueryAttrs = ['interactionStart', 'interactionEnd', 'activityIds']; +const gradedQueryAttrs = ungradedQueryAttrs.concat('questionIds'); -const parseResult = it => ({ - ...it, - avgDuration: parseFloat(it.avgDuration), - views: parseInt(it.views, 10) -}); +const parseResult = it => { + const result = { ...it, avgDuration: parseFloat(it.avgDuration) }; + if (has(it, 'views')) result.views = parseInt(it.views, 10); + if (has(it, 'submissions')) result.submissions = parseInt(it.submissions, 10); + if (has(it, 'correct')) result.correct = parseInt(it.correct, 10); + return result; +}; -const parseGradedResult = it => ({ - ...it, - avgDuration: parseFloat(it.avgDuration), - submissions: parseInt(it.submissions, 10), - correct: parseInt(it.correct, 10) -}); +const whereConditions = 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 listUngradedEvents({ cohortId, query, options }, res) { const fn = utils.build(UngradedEvent); - const { activityIds, uniqueViews, fromDate, toDate } = query; 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 = whereConditions({ cohortId, ...pick(query, ungradedQueryAttrs) }); const opts = { where, ...options, group, attributes, raw: true }; return UngradedEvent.findAndCountAll(opts).then(({ rows, count }) => { const items = map(rows, parseResult); @@ -47,7 +51,6 @@ function listUngradedEvents({ cohortId, query, options }, res) { function listGradedEvents({ cohortId, query, options }, res) { const fn = utils.build(GradedEvent); - const { activityIds, fromDate, toDate } = query; const group = [fn.column('questionId'), fn.column('activityId')]; const attributes = [ [fn.column('questionId'), 'questionId'], @@ -57,13 +60,10 @@ function listGradedEvents({ cohortId, query, options }, res) { [fn.average('duration'), 'avgDuration'], [fn.max('interactionEnd'), 'lastSubmitted'] ]; - 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 = whereConditions({ cohortId, ...pick(query, gradedQueryAttrs) }); const opts = { where, ...options, group, attributes, raw: true }; return GradedEvent.findAndCountAll(opts).then(({ rows, count }) => { - const items = map(rows, parseGradedResult); + const items = map(rows, parseResult); return res.jsend.success(({ items, total: count.length })); }); } @@ -84,14 +84,14 @@ 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 calculateDuration({ interactionStart, interactionEnd }) { + if (!interactionStart || !interactionEnd) return null; + return Math.ceil((interactionEnd - interactionStart) / 1000); +} From a1ac8621da1876b6a018ec15756266a1da6f4148 Mon Sep 17 00:00:00 2001 From: franosincic Date: Thu, 6 Jun 2019 09:54:57 +0200 Subject: [PATCH 3/3] =?UTF-8?q?Refactor=20=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/event/event.controller.js | 49 ++++++++++++++++---------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/server/event/event.controller.js b/server/event/event.controller.js index 82a359b..d5a22d8 100644 --- a/server/event/event.controller.js +++ b/server/event/event.controller.js @@ -1,8 +1,8 @@ const db = require('../common/database'); -const has = require('lodash/has'); 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 { CREATED } = HttpStatus; @@ -10,26 +10,8 @@ const { Op } = Sequelize; const commonAttrs = ['userId', 'activityId', 'interactionStart', 'interactionEnd']; const ungradedAttrs = ['progress'].concat(commonAttrs); const gradedAttrs = ['questionId', 'isCorrect', 'answer'].concat(commonAttrs); -const ungradedQueryAttrs = ['interactionStart', 'interactionEnd', 'activityIds']; -const gradedQueryAttrs = ungradedQueryAttrs.concat('questionIds'); - -const parseResult = it => { - const result = { ...it, avgDuration: parseFloat(it.avgDuration) }; - if (has(it, 'views')) result.views = parseInt(it.views, 10); - if (has(it, 'submissions')) result.submissions = parseInt(it.submissions, 10); - if (has(it, 'correct')) result.correct = parseInt(it.correct, 10); - return result; -}; - -const whereConditions = 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; -}; +const ungradedFilterAttrs = ['interactionStart', 'interactionEnd', 'activityIds']; +const gradedFilterAttrs = ungradedFilterAttrs.concat('questionIds'); function listUngradedEvents({ cohortId, query, options }, res) { const fn = utils.build(UngradedEvent); @@ -41,7 +23,7 @@ function listUngradedEvents({ cohortId, query, options }, res) { [fn.average('duration'), 'avgDuration'], [fn.max('interactionEnd'), 'lastViewed'] ]; - const where = whereConditions({ cohortId, ...pick(query, ungradedQueryAttrs) }); + 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); @@ -56,11 +38,11 @@ function listGradedEvents({ cohortId, query, options }, res) { [fn.column('questionId'), 'questionId'], [fn.column('activityId'), 'activityId'], [fn.sum(Sequelize.cast(fn.column('isCorrect'), 'integer')), 'correct'], - [fn.count(fn.distinct('userId')), 'submissions'], + [fn.count(fn.column('userId')), 'submissions'], [fn.average('duration'), 'avgDuration'], [fn.max('interactionEnd'), 'lastSubmitted'] ]; - const where = whereConditions({ cohortId, ...pick(query, gradedQueryAttrs) }); + 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); @@ -91,6 +73,25 @@ module.exports = { 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);