app/public/js/controllers/activity.js
/*
* Copyright 2016 e-UCM (http://www.e-ucm.es/)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* This project has received funding from the European Union’s Horizon
* 2020 research and innovation programme under grant agreement No 644187.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0 (link is external)
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
angular.module('activityApp', ['myApp', 'ngStorage', 'services'])
.factory('_', ['$window', function ($window) {
return $window._;
}])
.config(['$locationProvider',
function ($locationProvider) {
$locationProvider.html5Mode(false);
}
])
.directive('reports', function () {
return function (scope, element) {
new RadialProgress(angular.element(element).children('.progress-marker')[0], scope.result.progress);
new ColumnProgress(angular.element(element).children('.score-marker')[0], scope.result.score);
};
})
.directive('fileModel', ['$parse', '$rootScope', function ($parse, $rootScope) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
var model = $parse(attrs.fileModel);
var modelSetter = model.assign;
element.bind('change', function () {
$rootScope.$apply(function () {
modelSetter($rootScope, element[0].files[0]);
});
});
}
};
}])
.controller('ActivityCtrl', ['$rootScope', '$scope', '$attrs', '$location', '$http', 'Activities', 'Classes', '_',
'Results', 'Versions', 'Groups', 'Groupings', '$sce', '$interval', 'Role', 'CONSTANTS',
function ($rootScope, $scope, $attrs, $location, $http, Activities, Classes, _, Results,
Versions, Groups, Groupings, $sce, $interval, Role, CONSTANTS) {
var refresh;
var groupsReady = false;
var groupingsReady = false;
var classReady = false;
$scope.class = {};
var onSetActivity = function () {
Classes.get({classId: $scope.activity.classId}).$promise.then(function (c) {
classReady = true;
$scope.class = c;
if ($scope.activity.groupings && $scope.activity.groupings.length > 0) {
$scope.unlockGroupings();
} else if ($scope.activity.groups && $scope.activity.groups.length > 0) {
$scope.unlockGroups();
}
updateGroups();
updateGroupings();
$scope.refreshResults = function () {
if (Role.isUser()) {
if (!$scope.gameplaysShown) {
$scope.gameplaysShown = {};
}
if (Role.isTeacher()) {
Activities.attempts({activityId: $scope.activity._id}, function (attempts) {
$scope.attempts = attempts;
});
}
}
var rawResults = Results.query({
id: $scope.activity._id
},
function () {
calculateResults(rawResults);
});
};
if (!$attrs.lite) {
$scope.iframeDashboardUrl = dashboardLink();
$scope.studentIframe = dashboardLink($scope.$storage.user.username);
$scope.version = Versions.get({
gameId: $scope.activity.gameId,
versionId: $scope.activity.versionId
}, function () {
$scope.refreshResults();
if (!$scope.activity.end) {
refresh = $interval(function () {
$scope.refreshResults();
}, 10000);
}
});
}
});
};
var updateGroups = function () {
var route = CONSTANTS.PROXY + '/classes/' + $scope.class._id + '/groups';
$http.get(route).success(function (data) {
$scope.classGroups = data;
}).error(function (data, status) {
console.error('Error on put' + route + ' ' +
JSON.stringify(data) + ', status: ' + status);
});
};
var updateGroupings = function () {
var route = CONSTANTS.PROXY + '/classes/' + $scope.class._id + '/groupings';
$http.get(route).success(function (data) {
$scope.classGroupings = data;
}).error(function (data, status) {
console.error('Error on put' + route + ' ' +
JSON.stringify(data) + ', status: ' + status);
});
};
$scope.$on('$destroy', function () {
if (refresh) {
$interval.cancel(refresh);
}
});
$attrs.$observe('activityid', function () {
$scope.activity = Activities.get({activityId: $attrs.activityid}, onSetActivity);
updateOfflineTraces();
});
$attrs.$observe('activity', function () {
groupsReady = false;
groupingsReady = false;
classReady = false;
$scope.activity = JSON.parse($attrs.activity);
Activities.get({activityId: $scope.activity._id}).$promise.then(function (a) {
$scope.activity = a;
onSetActivity();
updateOfflineTraces();
});
});
$scope.student = {};
$scope.teacher = {};
var evalExpression = function (expression, defaultValue) {
try {
return eval(expression) || defaultValue;
} catch (err) {
return defaultValue;
}
};
var dashboardLink = function (userName, attempt) {
var url = CONSTANTS.KIBANA + '/app/kibana#/dashboard/dashboard_' +
$scope.activity._id + '?embed=true_g=(refreshInterval:(display:\'5%20seconds\',' +
'pause:!f,section:1,value:5000),time:(from:now-1h,mode:quick,to:now))';
if (url.startsWith('localhost')) {
url = 'http://' + url;
}
var filter = {};
if (userName) {
filter['out.name'] = userName;
} else if ($scope.player) {
filter['out.name'] = $scope.player.name;
}
if (attempt) {
filter['out.session'] = attempt;
} else if ($scope.attempt) {
filter['out.session'] = $scope.attempt.number;
}
if (Object.keys(filter).length > 0) {
var props = [];
for (var key in filter) {
if (filter.hasOwnProperty(key)) {
props.push(key + ':' + filter[key]);
}
}
url += '&_a=(filters:!(),options:(darkTheme:!f),query:(query_string:(analyze_wildcard:!t,query:\'' + props.join(',') + '\')))';
}
if (url.startsWith('localhost')) {
url = 'http://' + url;
}
return $sce.trustAsResourceUrl(url);
};
var calculateResults = function (rawResults) {
var results = [];
rawResults.forEach(function (result) {
$scope.version.alias = $scope.version.alias ? $scope.version.alias : 'this.name';
result.name = evalExpression.call(result, $scope.version.alias, 'Unknown');
result.warnings = [];
for (var i = 0; $scope.version.warnings && i < $scope.version.warnings.length; i++) {
var warning = $scope.version.warnings[i];
if (evalExpression.call(result, warning.cond, false)) {
result.warnings.push(i);
}
}
result.alerts = [];
for (i = 0; $scope.version.alerts && i < $scope.version.alerts.length; i++) {
var alert = $scope.version.alerts[i];
var level = evalExpression.call(result, alert.value, 0);
if (level - ((result.levels && result.levels[i]) || 0) >= alert.maxDiff) {
result.alerts.push({
id: i,
level: level
});
}
}
results.push(result);
if ($scope.player && $scope.player._id === result._id) {
$scope.player = result;
}
});
$scope.results = results;
};
$scope.viewAll = function () {
$scope.player = null;
$scope.attempt = null;
$scope.iframeDashboardUrl = dashboardLink();
};
$scope.viewPlayer = function (result) {
$scope.player = result;
$scope.attempt = null;
$scope.iframeDashboardUrl = dashboardLink(result.name);
};
$scope.viewAttempt = function (gameplay, attempt) {
if ($scope.results) {
var lookForName = gameplay.playerType === 'anonymous' ? gameplay.animalName : gameplay.playerName;
for (var i = 0; i < $scope.results.length; i++) {
if ($scope.results[i].name === lookForName) {
$scope.player = $scope.results[i];
break;
}
}
}
// This code manually changes the tab, this might be solved with tab('show') in newer versions
// as mentioned in https://github.com/twbs/bootstrap/issues/23594
$('.active[data-toggle=\'tab\']').toggleClass('active').toggleClass('show');
$('span[href=\'#realtime\'][data-toggle=\'tab\']').toggleClass('active').toggleClass('show');
$('.tab-pane.active').toggleClass('active').toggleClass('show');
$('#realtime').toggleClass('active').toggleClass('show');
$scope.attempt = attempt;
$scope.iframeDashboardUrl = dashboardLink();
};
function updateOfflineTraces() {
if ($scope.activity && $scope.activity.offline) {
$http.get(CONSTANTS.PROXY + '/offlinetraces/' + $scope.activity._id, {
transformRequest: angular.identity,
headers: {
'Content-Type': undefined,
enctype: 'multipart/form-data'
}
}).then(function successCallback(response) {
// This callback will be called asynchronously
// when the response is available
$scope.offlinetraces = response.data;
}, function errorCallback(response) {
// Called asynchronously if an error occurs
// or server returns response with an error status.
console.error('Error on get /offlinetraces/' + $scope.activity._id + ' ' +
JSON.stringify(response, null, ' '));
});
$http.get(CONSTANTS.PROXY + '/offlinetraces/' + $scope.activity._id + '/kahoot', {
transformRequest: angular.identity,
headers: {
'Content-Type': undefined,
enctype: 'multipart/form-data'
}
}).then(function successCallback(response) {
// This callback will be called asynchronously
// when the response is available
$scope.offlinetraceskahoot = response.data;
}, function errorCallback(response) {
// Called asynchronously if an error occurs
// or server returns response with an error status.
console.error('Error on get /offlinetraces/' + $scope.activity._id + '/kahoot ' +
JSON.stringify(response, null, ' '));
});
}
}
function upload(file, kahoot) {
var appended = 'offlinetraces';
var url = CONSTANTS.PROXY + '/offlinetraces/' + $scope.activity._id;
if (kahoot) {
url += '/kahoot';
appended += 'kahoot';
}
var formData = new FormData();
formData.append(appended, file);
$http.post(url, formData, {
transformRequest: angular.identity,
headers: {
'Content-Type': undefined,
enctype: 'multipart/form-data'
}
}).then(function successCallback(response) {
// This callback will be called asynchronously
// when the response is available
updateOfflineTraces();
}, function errorCallback(response) {
// Called asynchronously if an error occurs
// or server returns response with an error status.
console.error('Error on post ' + url + ' ' +
JSON.stringify(response, null, ' '));
});
}
$rootScope.tracesFile = undefined;
$scope.uploadTracesFile = function () {
if ($rootScope.tracesFile) {
upload($rootScope.tracesFile);
}
};
$scope.uploadKahootTracesFile = function () {
if ($rootScope.kahootTracesFile) {
upload($rootScope.kahootTracesFile, true);
}
};
// Anonymous
$scope.anonymous = 'btn-default';
$scope.allowAnonymous = function () {
$scope.activity.$update();
};
// Students
$scope.classGroups = [];
$scope.classGroupings = [];
$scope.selectedGroup = undefined;
$scope.selectedGrouping = undefined;
$scope.unlockedGroups = false;
$scope.unlockedGroupings = false;
$scope.isUsingGroupings = function () {
return $scope.activity.groupings && $scope.activity.groupings.length > 0;
};
$scope.isUsingGroups = function () {
return !$scope.isUsingGroupings() && $scope.activity.groups && $scope.activity.groups.length > 0;
};
$scope.unlockGroups = function () {
var route = CONSTANTS.PROXY + '/activities/' + $scope.activity._id + '/remove';
if ($scope.unlockedGroupings) {
$http.put(route, {groupings: $scope.activity.groupings}).success(function (data) {
$scope.activity = data;
$scope.unlockedGroupings = false;
}).error(function (data, status) {
console.error('Error on put' + route + ' ' +
JSON.stringify(data) + ', status: ' + status);
});
}
if ($scope.unlockedGroups) {
$http.put(route, {groups: $scope.activity.groups}).success(function (data) {
$scope.activity = data;
$scope.unlockedGroups = false;
}).error(function (data, status) {
console.error('Error on put' + route + ' ' +
JSON.stringify(data) + ', status: ' + status);
});
} else {
$scope.unlockedGroups = true;
}
};
$scope.unlockGroupings = function () {
var route = CONSTANTS.PROXY + '/activities/' + $scope.activity._id + '/remove';
if ($scope.unlockedGroups) {
$http.put(route, {groups: $scope.activity.groups}).success(function (data) {
$scope.activity = data;
$scope.unlockedGroups = false;
}).error(function (data, status) {
console.error('Error on put' + route + ' ' +
JSON.stringify(data) + ', status: ' + status);
});
}
if ($scope.unlockedGroupings) {
$http.put(route, {groupings: $scope.activity.groupings}).success(function (data) {
$scope.activity = data;
$scope.unlockedGroupings = false;
}).error(function (data, status) {
console.error('Error on put' + route + ' ' +
JSON.stringify(data) + ', status: ' + status);
});
} else {
$scope.unlockedGroupings = true;
}
};
$scope.selectGroup = function (group) {
if ($scope.selectedGroup && $scope.selectedGroup._id === group._id) {
$scope.selectedGroup = undefined;
} else {
$scope.selectedGroup = group;
}
$scope.selectedGrouping = undefined;
};
$scope.isInSelectedGroup = function (usr, role, group) {
if (group) {
return group.participants[role].indexOf(usr) !== -1;
}
if ($scope.selectedGroup) {
return $scope.selectedGroup.participants[role].indexOf(usr) !== -1;
}
return false;
};
$scope.selectGrouping = function (grouping) {
if ($scope.selectedGrouping && $scope.selectedGrouping._id === grouping._id) {
$scope.selectedGrouping = undefined;
} else {
$scope.selectedGrouping = grouping;
}
$scope.selectedGroup = undefined;
};
$scope.getGroupThClass = function (group) {
if ($scope.selectedGroup && $scope.selectedGroup._id === group._id) {
return 'bg-success';
}
if ($scope.selectedGrouping && $scope.isInSelectedGrouping(group._id, 'group')) {
return 'bg-warning';
}
return '';
};
$scope.getUserThClass = function (usr, role) {
if ($scope.selectedGroup && $scope.isInSelectedGroup(usr, role)) {
return 'bg-success';
}
if ($scope.selectedGrouping && $scope.isInSelectedGrouping(usr, role)) {
return 'bg-warning';
}
return '';
};
$scope.isInSelectedGrouping = function (id, role) {
if ($scope.selectedGrouping) {
if (role === 'group') {
return $scope.selectedGrouping.groups.indexOf(id) !== -1;
}
for (var i = 0; i < $scope.selectedGrouping.groups.length; i++) {
for (var j = 0; j < $scope.classGroups.length; j++) {
if ($scope.classGroups[j]._id === $scope.selectedGrouping.groups[i]) {
if ($scope.isInSelectedGroup(id, role, $scope.classGroups[j])) {
return true;
}
}
}
}
}
return false;
};
$scope.checkGroup = function (group) {
var route = CONSTANTS.PROXY + '/activities/' + $scope.activity._id;
if ($scope.activity.groups && $scope.activity.groups.indexOf(group._id) !== -1) {
route += '/remove';
}
$http.put(route, {groups: [group._id]}).success(function (data) {
$scope.activity = data;
}).error(function (data, status) {
console.error('Error on put' + route + ' ' +
JSON.stringify(data) + ', status: ' + status);
});
};
$scope.checkGrouping = function (grouping) {
var route = CONSTANTS.PROXY + '/activities/' + $scope.activity._id;
if ($scope.activity.groupings && $scope.activity.groupings.indexOf(grouping._id) !== -1) {
route += '/remove';
}
$http.put(route, {groupings: [grouping._id]}).success(function (data) {
$scope.activity = data;
}).error(function (data, status) {
console.error('Error on put' + route + ' ' +
JSON.stringify(data) + ', status: ' + status);
});
};
// Name
$scope.changeActivityName = function () {
$scope.activity.$update(function () {
$rootScope.$broadcast('refreshClasses');
});
};
// Realtime control
/**
* ActivityState returns the state of the activity from one of the next possible states:
* - 0: Stopped
* - 1: Loading
* - 2: Open
*
* @param activity
* @returns {*|boolean}
*/
$scope.activityState = function () {
if (!$scope.activity || $scope.activity.loading) {
return 1;
}
return $scope.activity.start && !$scope.activity.end ? 2 : 0;
};
$scope.$on('refreshActivity', function (evt, activity) {
if ($scope.activity._id === activity._id) {
$scope.activity = activity;
updateOfflineTraces();
}
});
var finishEvent = function (activity) {
$scope.activity = activity;
$rootScope.$broadcast('refreshActivity', $scope.activity);
};
$scope.startActivity = function () {
if (!$scope.activity || $scope.activity.loading) {
return;
}
$scope.activity.$event({event: 'start'}).$promise.then(finishEvent).fail(function (error) {
console.error(error);
$.notify('<strong>Error while opening the activity:</strong><br>If the session was recently closed it ' +
'might need to be cleaned by the system. <br>Please try again in a few seconds.', {
offset: {x: 10, y: 65},
type: 'danger'// jscs:ignore requireCamelCaseOrUpperCaseIdentifiers
});
$rootScope.$broadcast('refreshActivity', $scope.activity);
});
};
$scope.endActivity = function () {
if (!$scope.activity || $scope.activity.loading) {
return;
}
$scope.activity.$event({event: 'end'}).$promise.then(finishEvent).fail(function (error) {
console.error(error);
$.notify('<strong>Error while closing the activity:</strong><br>Please try again in a few seconds.', {
offset: {x: 10, y: 65},
type: 'danger'// jscs:ignore requireCamelCaseOrUpperCaseIdentifiers
});
$rootScope.$broadcast('refreshActivity', $scope.activity);
});
};
$scope.$watch('iframeDashboardUrl', function (newValue, oldValue) {
var iframeObj = document.getElementById('dashboardIframe');
if (iframeObj) {
iframeObj.src = newValue;
iframeObj.contentWindow.location.reload();
}
});
$scope.$watch('studentIframe', function (newValue, oldValue) {
var iframeObj = document.getElementById('dashboardIframeStudent');
if (iframeObj) {
iframeObj.src = newValue;
iframeObj.contentWindow.location.reload();
}
});
$scope.updateLevels = function (player) {
var levels = player.levels || [];
player.alerts.forEach(function (alert) {
levels[alert.id] = alert.level;
});
delete player.alerts;
player.levels = levels;
player.$save({id: $scope.activity._id}, function () {
$scope.player = null;
$scope.refreshResults();
});
};
$scope.deleteUserData = function (name) {
$http.delete(CONSTANTS.PROXY + '/activities/data/' + $scope.activity._id + '/' + name).success(function () {
}).error(function (err) {
console.error(err);
});
};
}
]);