diff --git a/lib/api/profile/index.js b/lib/api/profile/index.js index 30f8fe240ca..faae33518ec 100644 --- a/lib/api/profile/index.js +++ b/lib/api/profile/index.js @@ -16,6 +16,31 @@ function configure (app, wares, ctx) { api.use(wares.bodyParser.urlencoded({ extended: true })); api.use(ctx.authorization.isPermitted('api:profile:read')); + + + /** + * @function query_models + * Perform the standard query logic, translating API parameters into mongo + * db queries in a fairly regimented manner. + * This middleware executes the query, returning the results as JSON + */ + function query_models (req, res, next) { + var query = req.query; + + // If "?count=" is present, use that number to decide how many to return. + if (!query.count) { + query.count = consts.ENTRIES_DEFAULT_COUNT; + } + + // perform the query + ctx.profile.list_query(query, function payload(err, profiles) { + return res.json(profiles); + }); + } + + // List profiles available + api.get('/profiles/', query_models); + // List profiles available api.get('/profile/', function(req, res) { ctx.profile.list(function (err, attribute) { diff --git a/lib/client/browser-settings.js b/lib/client/browser-settings.js index 2119b39da40..7b24348e75a 100644 --- a/lib/client/browser-settings.js +++ b/lib/client/browser-settings.js @@ -256,6 +256,7 @@ function init (client, serverSettings, $) { settings.thresholds = serverSettings.settings.thresholds; } + if (serverSettings.settings.enable) { settings.enable = serverSettings.settings.enable; } diff --git a/lib/profilefunctions.js b/lib/profilefunctions.js index 8bd251d4820..5ae6435c4c1 100644 --- a/lib/profilefunctions.js +++ b/lib/profilefunctions.js @@ -142,22 +142,28 @@ function init (profileData) { }; profile.getCurrentProfile = function getCurrentProfile (time, spec_profile) { + if (spec_profile) { + return spec_profile; + } else { + time = time || new Date().getTime(); + + time = time || Date.now(); + var minuteTime = Math.round(time / 60000) * 60000; + var cacheKey = ("profile" + minuteTime + spec_profile); + var returnValue = cache.get(cacheKey); + + if (returnValue) { + return returnValue; + } - time = time || Date.now(); - var minuteTime = Math.round(time / 60000) * 60000; - var cacheKey = ("profile" + minuteTime + spec_profile); - var returnValue = cache.get(cacheKey); + var pdataActive = profile.profileFromTime(time); + var data = profile.hasData() ? pdataActive : null; + var timeprofile = profile.activeProfileToTime(time); + returnValue = data && data.store[timeprofile] ? data.store[timeprofile] : {}; - if (returnValue) { + cache.put(cacheKey, returnValue, cacheTTL); return returnValue; } - - var data = profile.hasData() ? profile.data[0] : null; - var timeprofile = spec_profile || profile.activeProfileToTime(time); - returnValue = data && data.store[timeprofile] ? data.store[timeprofile] : {}; - - cache.put(cacheKey, returnValue, cacheTTL); - return returnValue; }; profile.getUnits = function getUnits (spec_profile) { @@ -225,10 +231,13 @@ function init (profileData) { profile.activeProfileToTime = function activeProfileToTime (time) { if (profile.hasData()) { - var timeprofile = profile.data[0].defaultProfile; time = Number(time) || new Date().getTime(); + + var pdataActive = profile.profileFromTime(time); + var timeprofile = pdataActive.defaultProfile; var treatment = profile.activeProfileTreatmentToTime(time); - if (treatment && profile.data[0].store && profile.data[0].store[treatment.profile]) { + + if (treatment && pdataActive.store && pdataActive.store[treatment.profile]) { timeprofile = treatment.profile; } return timeprofile; @@ -248,30 +257,31 @@ function init (profileData) { var treatment = null; if (profile.hasData()) { - profile.profiletreatments.forEach(function eachTreatment (t) { - if (time >= t.mills && t.mills >= profile.data[0].mills) { - var duration = times.mins(t.duration || 0).msecs; - if (duration != 0 && time < t.mills + duration) { - treatment = t; - // if profile switch contains json of profile inject it in to store to be findable by profile name - if (treatment.profileJson && !profile.data[0].store[treatment.profile]) { - if (treatment.profile.indexOf("@@@@@") < 0) - treatment.profile += "@@@@@" + treatment.mills; - let json = JSON.parse(treatment.profileJson); - profile.data[0].store[treatment.profile] = json; - } - } - if (duration == 0) { - treatment = t; - // if profile switch contains json of profile inject it in to store to be findable by profile name - if (treatment.profileJson && !profile.data[0].store[treatment.profile]) { - if (treatment.profile.indexOf("@@@@@") < 0) - treatment.profile += "@@@@@" + treatment.mills; - let json = JSON.parse(treatment.profileJson); - profile.data[0].store[treatment.profile] = json; - } + var pdataActive = profile.profileFromTime(time); + profile.profiletreatments.forEach(function eachTreatment(t) { + if (time >= t.mills && t.mills >= pdataActive.mills) { + var duration = times.mins(t.duration || 0).msecs; + if (duration != 0 && time < t.mills + duration) { + treatment = t; + // if profile switch contains json of profile inject it in to store to be findable by profile name + if (treatment.profileJson && !pdataActive.store[treatment.profile]) { + if (treatment.profile.indexOf("@@@@@") < 0) + treatment.profile += "@@@@@" + treatment.mills; + let json = JSON.parse(treatment.profileJson); + pdataActive.store[treatment.profile] = json; + } + } + if (duration == 0) { + treatment = t; + // if profile switch contains json of profile inject it in to store to be findable by profile name + if (treatment.profileJson && !pdataActive.store[treatment.profile]) { + if (treatment.profile.indexOf("@@@@@") < 0) + treatment.profile += "@@@@@" + treatment.mills; + let json = JSON.parse(treatment.profileJson); + pdataActive.store[treatment.profile] = json; + } + } } - } }); } @@ -286,6 +296,23 @@ function init (profileData) { else return name.substring(0, index); } + profile.profileFromTime = function profileFromTime (time) { + var profileData = null; + + if (profile.hasData()) { + profileData = profile.data[0]; + for (var i = 0; i < profile.data.length; i++) + { + if (Number(time) >= Number(profile.data[i].mills)) { + profileData = profile.data[i]; + break; + } + } + } + + return profileData; + } + profile.tempBasalTreatment = function tempBasalTreatment (time) { // Most queries for the data in reporting will match the latest found value, caching that hugely improves performance diff --git a/lib/report_plugins/daytoday.js b/lib/report_plugins/daytoday.js index 9348bd76e8f..093cbeab042 100644 --- a/lib/report_plugins/daytoday.js +++ b/lib/report_plugins/daytoday.js @@ -452,6 +452,8 @@ daytoday.report = function report_daytoday (datastorage, sorteddaystoshow, optio data.netBasalNegative[hour] = 0; }); + profile.loadData(datastorage.profiles); + profile.updateTreatments(datastorage.profileSwitchTreatments, datastorage.tempbasalTreatments, datastorage.combobolusTreatments); var bolusInsulin = 0; diff --git a/lib/report_plugins/profiles.js b/lib/report_plugins/profiles.js index 4aea2d040a8..f3bd9eb8d21 100644 --- a/lib/report_plugins/profiles.js +++ b/lib/report_plugins/profiles.js @@ -90,10 +90,12 @@ profiles.report = function report_profiles (datastorage) { function displayRanges (array, array2) { var text = ''; - for (var i = 0; i < array.length; i++) { - text += array[i].time + ' : ' + array[i].value + (array2 ? ' - ' + array2[i].value : '') + '
'; - } + if (array && array2) { + for (var i = 0; i < array.length; i++) { + text += array[i].time + ' : ' + array[i].value + (array2 ? ' - ' + array2[i].value : '') + '
'; + } + } return text; } }; diff --git a/lib/server/profile.js b/lib/server/profile.js index d456b590959..e4c43504256 100644 --- a/lib/server/profile.js +++ b/lib/server/profile.js @@ -1,5 +1,7 @@ 'use strict'; +var find_options = require('./query'); + function storage (collection, ctx) { var ObjectID = require('mongodb').ObjectID; @@ -27,6 +29,48 @@ function storage (collection, ctx) { return api( ).find({ }).sort({startDate: -1}).toArray(fn); } + function list_query (opts, fn) { + + storage.queryOpts = { + walker: {} + , dateField: 'startDate' + }; + + function limit () { + if (opts && opts.count) { + return this.limit(parseInt(opts.count)); + } + return this; + } + + return limit.call(api() + .find(query_for(opts)) + .sort(opts && opts.sort && query_sort(opts) || { startDate: -1 }), opts) + .toArray(fn); + } + + function query_for (opts) { + var retVal = find_options(opts, storage.queryOpts); + return retVal; + } + + function query_sort (opts) { + if (opts && opts.sort) { + var sortKeys = Object.keys(opts.sort); + + for (var i = 0; i < sortKeys.length; i++) { + if (opts.sort[sortKeys[i]] == '1') { + opts.sort[sortKeys[i]] = 1; + } + else { + opts.sort[sortKeys[i]] = -1; + } + } + return opts.sort; + } + } + + function last (fn) { return api().find().sort({startDate: -1}).limit(1).toArray(fn); } @@ -43,6 +87,7 @@ function storage (collection, ctx) { } api.list = list; + api.list_query = list_query; api.create = create; api.save = save; api.remove = remove; diff --git a/static/report/js/report.js b/static/report/js/report.js index f7b02ec44af..b0cbd1abab8 100644 --- a/static/report/js/report.js +++ b/static/report/js/report.js @@ -459,11 +459,14 @@ if (loadeddays === dayscount) { sorteddaystoshow.sort(); var from = sorteddaystoshow[0]; + var dFrom = sorteddaystoshow[0]; + var dTo = sorteddaystoshow[(sorteddaystoshow.length - 1)]; + if (options.order === report_plugins.consts.ORDER_NEWESTONTOP) { sorteddaystoshow.reverse(); } - loadProfileSwitch(from, function loadProfileSwitchCallback() { - loadProfiles(function loadProfilesCallback() { + loadProfileSwitch(dFrom, function loadProfileSwitchCallback() { + loadProfilesRange(dFrom, dTo, sorteddaystoshow.length, function loadProfilesCallback() { $('#info > b').html('' + translate('Rendering') + ' ...'); window.setTimeout(function () { showreports(options); @@ -709,7 +712,7 @@ }); } - function loadProfileSwitch(from, callback) { + function loadProfileSwitch (from, callback) { $('#info > b').html(''+translate('Loading profile switch data') + ' ...'); var tquery = '?find[eventType]=Profile Switch' + '&find[created_at][$lte]=' + new Date(from).toISOString() + '&count=1'; $.ajax('/api/v1/treatments.json'+tquery, { @@ -743,6 +746,69 @@ }).done(callback); } + function loadProfilesRange (dateFrom, dateTo, dayCount, callback) { + $('#info > b').html('' + translate('Loading profile range') + ' ...'); + + $.when( + loadProfilesRangeCore(dateFrom, dateTo, dayCount), + loadProfilesRangePrevious(dateFrom), + loadProfilesRangeNext(dateTo) + ) + .done(callback) + .fail(function () { + datastorage.profiles = []; + }); + } + + function loadProfilesRangeCore (dateFrom, dateTo, dayCount) { + $('#info > b').html('' + translate('Loading core profiles') + ' ...'); + + //The results must be returned in descending order to work with key logic in routines such as getCurrentProfile + var tquery = '?find[startDate][$gte]=' + new Date(dateFrom).toISOString() + '&find[startDate][$lte]=' + new Date(dateTo).toISOString() + '&sort[startDate]=-1&count=' + dayCount; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers(), + async: false, + success: function (records) { + datastorage.profiles = records; + } + }); + } + + function loadProfilesRangePrevious (dateFrom) { + $('#info > b').html('' + translate('Loading previous profile') + ' ...'); + + //Find first one before the start date and add to datastorage.profiles + var tquery = '?find[startDate][$lt]=' + new Date(dateFrom).toISOString() + '&sort[startDate]=-1&count=1'; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers(), + async: false, + success: function (records) { + records.forEach(function (r) { + datastorage.profiles.push(r); + }); + } + }); + } + + function loadProfilesRangeNext (dateTo) { + $('#info > b').html('' + translate('Loading next profile') + ' ...'); + + //Find first one after the end date and add to datastorage.profiles + var tquery = '?find[startDate][$gt]=' + new Date(dateTo).toISOString() + '&sort[startDate]=1&count=1'; + + return $.ajax('/api/v1/profiles' + tquery, { + headers: client.headers(), + async: false, + success: function (records) { + records.forEach(function (r) { + //must be inserted as top to maintain profiles being sorted by date in descending order + datastorage.profiles.unshift(r); + }); + } + }); + } function processData(data, day, options, callback) { if (daystoshow[day].treatmentsonly) { diff --git a/tests/profile.test.js b/tests/profile.test.js index 373f0479d9d..5928ccd2618 100644 --- a/tests/profile.test.js +++ b/tests/profile.test.js @@ -188,5 +188,199 @@ describe('Profile', function ( ) { dia.should.equal(9); }); + var multiProfileData = + [ + { + "startDate": "2015-06-25T00:00:00.000Z", + "defaultProfile": "20150625-1", + "store": { + "20150625-1": { + "dia": "4", + "timezone": moment.tz().zoneName(), //Assume these are in the localtime zone so tests pass when not on UTC time + "startDate": "1970-01-01T00:00:00.000Z", + 'sens': [ + { + 'time': '00:00', + 'value': 12 + }, + { + 'time': '02:00', + 'value': 13 + }, + { + 'time': '07:00', + 'value': 14 + } + ], + 'carbratio': [ + { + 'time': '00:00', + 'value': 16 + }, + { + 'time': '06:00', + 'value': 15 + }, + { + 'time': '14:00', + 'value': 17 + } + ], + 'carbs_hr': 30, + 'target_low': 4.5, + 'target_high': 8, + "units": "mmol", + "basal": [ + { + "time": "00:00", + "value": "0.5", + "timeAsSeconds": "0" + }, + { + "time": "09:00", + "value": "0.25", + "timeAsSeconds": "32400" + }, + { + "time": "12:30", + "value": "0.9", + "timeAsSeconds": "45000" + }, + { + "time": "17:00", + "value": "0.3", + "timeAsSeconds": "61200" + }, + { + "time": "20:00", + "value": "1", + "timeAsSeconds": "72000" + } + ] + } + }, + "units": "mmol", + "mills": "1435190400000" + }, + { + "startDate": "2015-06-21T00:00:00.000Z", + "defaultProfile": "20190621-1", + "store": { + "20190621-1": { + "dia": "4", + "timezone": moment.tz().zoneName(), //Assume these are in the localtime zone so tests pass when not on UTC time + "startDate": "1970-01-01T00:00:00.000Z", + 'sens': [ + { + 'time': '00:00', + 'value': 11 + }, + { + 'time': '02:00', + 'value': 10 + }, + { + 'time': '07:00', + 'value': 9 + } + ], + 'carbratio': [ + { + 'time': '00:00', + 'value': 12 + }, + { + 'time': '06:00', + 'value': 13 + }, + { + 'time': '14:00', + 'value': 14 + } + ], + 'carbs_hr': 35, + 'target_low': 4.2, + 'target_high': 9, + "units": "mmol", + "basal": [ + { + "time": "00:00", + "value": "0.3", + "timeAsSeconds": "0" + }, + { + "time": "09:00", + "value": "0.4", + "timeAsSeconds": "32400" + }, + { + "time": "12:30", + "value": "0.5", + "timeAsSeconds": "45000" + }, + { + "time": "17:00", + "value": "0.6", + "timeAsSeconds": "61200" + }, + { + "time": "23:00", + "value": "0.7", + "timeAsSeconds": "82800" + } + ] + } + }, + "units": "mmol", + "mills": "1434844800000" + } + ]; + + var multiProfile = require('../lib/profilefunctions')(multiProfileData); + + var noon = new Date('2015-06-22 12:00:00').getTime(); + var threepm = new Date('2015-06-26 15:00:00').getTime(); + + it('should return profile units when configured', function () { + var value = multiProfile.getUnits(); + value.should.equal('mmol'); + }); + + + it('should know what the basal rate is at 12:00 with multiple profiles', function () { + var value = multiProfile.getBasal(noon); + value.should.equal(0.4); + }); + + it('should know what the basal rate is at 15:00 with multiple profiles', function () { + var value = multiProfile.getBasal(threepm); + value.should.equal(0.9); + }); + + it('should know what the carbratio is at 12:00 with multiple profiles', function () { + var carbRatio = multiProfile.getCarbRatio(noon); + carbRatio.should.equal(13); + }); + + it('should know what the carbratio is at 15:00 with multiple profiles', function () { + var carbRatio = multiProfile.getCarbRatio(threepm); + carbRatio.should.equal(17); + }); + + it('should know what the sensitivity is at 12:00 with multiple profiles', function () { + var dia = multiProfile.getSensitivity(noon); + dia.should.equal(9); + }); + + it('should know what the sensitivity is at 15:00 with multiple profiles', function () { + var dia = multiProfile.getSensitivity(threepm); + dia.should.equal(14); + }); + + + it('should select the correct profile for 15:00 with multiple profiles', function () { + var curProfile = multiProfile.getCurrentProfile(threepm); + curProfile.carbs_hr.should.equal(30); + }); }); \ No newline at end of file