text: (function () {
  
      angular
          .module('app')
          .run(run);
  
      run.$inject = [
          '$rootScope',
          'authService', 'authManager', 'sitealertsService'
      ];
  
      function run($rootScope, authService, authManager, sitealertsService) {
          // Put the authService on $rootScope so its methods
          // can be accessed from the nav bar
          $rootScope.authService = authService;
  
          // register auth listener
          authService.setupAuth();
  
          // check auth when they load or refresh the page
          authManager.checkAuthOnRefresh();
  
          // send them back to the login screen if they get an HTTP 401
          //  from an API call
          authManager.redirectWhenUnauthenticated();
  
          // display confirmation if the user is verifying their
          //  email address with Auth0
          authService.checkForAuthMessagesInUrl();
  
          // prepare the service for fetching site alerts
          sitealertsService.init();
      }
  })();
  
  var AUTH0_CLIENT_ID;
  var AUTH0_DOMAIN;
  var AUTH0_CUSTOM_DOMAIN;
  var AUTH0_CDN_BASE;
  var AUTH0_CALLBACK_URL=window.location.href;
  var AUTH0_AUDIENCE='audience';
  var REQUESTED_SCOPES='openid email write:training';
  
  (function () {
  
      angular
         .module('app')
         .controller('AdminController', AdminController);
  
      AdminController.$inject = [
          'authService', 'sitealertsService', '$scope'
      ];
  
      function AdminController(authService, sitealertsService, $scope) {
  
          function logError(err) {
              console.log(err);
          }
  
          var vm = this;
          vm.authService = authService;
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  $scope.expiry = 1;
                  $scope.url = 'https://cloud.ibm.com/status?query=EVENTID&selected=status';
              })
              .catch(logError);
  
          vm.confirm = function (newAlert) {
              newAlert.expiry = parseFloat(newAlert.expiry) * 3600000;
              sitealertsService.createAlert(newAlert)
                  .then(function () {
                      alert('done');
                  })
                  .catch(logError);
          };
  
          vm.refresh = function () {
              sitealertsService.refreshServer().catch(logError);
          };
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('AppInventorController', AppInventorController);
  
      AppInventorController.$inject = [
          'authService', 'projectsService', 'scratchkeysService',
          '$stateParams', '$scope'
      ];
  
      function AppInventorController(authService, projectsService, scratchkeysService, $stateParams, $scope) {
  
          var vm = this;
          vm.authService = authService;
  
          $scope.projectId = $stateParams.projectId;
          $scope.userId = $stateParams.userId;
  
          $scope.projecturls = {
              appinventor : 'http://ai2.appinventor.mit.edu/',
              train : '/#!/mlproject/' + $stateParams.userId + '/' + $stateParams.projectId + '/training',
              learnandtest : '/#!/mlproject/' + $stateParams.userId + '/' + $stateParams.projectId + '/models'
          };
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  return projectsService.getProject($scope.projectId, $scope.userId, profile.tenant);
              })
              .then(function (project) {
                  $scope.project = project;
  
                  $scope.projecturls.train = '/#!/mlproject/' + $scope.project.userid + '/' + $scope.project.id + '/training';
                  $scope.projecturls.learnandtest = '/#!/mlproject/' + $scope.project.userid + '/' + $scope.project.id + '/models';
  
                  return scratchkeysService.getScratchKeys($stateParams.projectId, $stateParams.userId, vm.profile.tenant);
              })
              .then(function (resp) {
                  if (resp) {
                      $scope.scratchkey = resp[0];
                      $scope.appinventorurl = window.location.origin +
                                              '/api/appinventor/' +
                                              $scope.scratchkey.id +
                                              '/extension';
                  }
              })
              .catch(function (err) {
                  $scope.failure = {
                      message : err.data.error,
                      status : err.status
                  };
              });
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .service('authService', authService);
  
      if (AUTH0_CLIENT_ID) {
          authService.$inject = [
              'authManager',
              '$q', '$http',
              '$mdDialog',
              '$rootScope',
              '$window',
              '$state',
              '$timeout',
              'lock'
          ];
      }
      else {
          authService.$inject = [
              'authManager',
              '$q', '$http',
              '$mdDialog',
              '$rootScope',
              '$window',
              '$state',
              '$timeout'
          ];
      }
  
  
      function authService(authManager, $q, $http, $mdDialog, $rootScope, $window, $state, $timeout, lock) {
  
          var SESSION_USERS_CLASS = 'session-users';
  
          // To avoid a session ever expiring, the UI will try to silently refresh
          //  access tokens in the background.
          // We'll do this 10 minutes before the token expires, to avoid risking a
          //  timing window where we're too late to do it.
          // This constant is the time before the token expiry when we'll try doing it.
          var TEN_MINUTES_MILLISECS = 600000;
  
          var vm = this;
  
          // If the user is logged in as a registered user, we'll try and refresh
          //  their access token for them before it expires, to prevent them from
          //  being logged off while active.
          // This timer is used to do that.
          var nextRefreshTimer = null;
  
          $rootScope.isTeacher = false;
          $rootScope.isAuthenticated = false;
  
          confirmLocalStorage();
  
          var userProfileStr = window.localStorageObj.getItem('profile');
          var userProfile = null;
          if (userProfileStr) {
              userProfile = JSON.parse(userProfileStr);
          }
          var deferredProfile = $q.defer();
  
          if (userProfile) {
              if (hasSessionExpired()) {
                  // We found an access token in local storage, but it
                  // has expired, so we'll wipe it and force them to
                  // log in again.
                  logout();
              }
              else {
                  deferredProfile.resolve(userProfile);
  
                  $rootScope.isTeacher = (userProfile.role === 'supervisor');
                  $rootScope.isAuthenticated = true;
  
                  if (Sentry) {
                      Sentry.configureScope(function (scope) {
                          scope.setUser({
                              email : userProfile.email,
                              username : userProfile.user_id
                          });
                          scope.setExtra('role', userProfile.role);
                          scope.setExtra('tenant', userProfile.tenant);
                      });
                  }
  
                  // Try to keep the user logged in while they're still active
                  //  by refreshing their access token for them in the background
                  // This can't be done for "Try it now" users, as those sessions
                  //  can't be renewed.
                  if (userProfile.tenant !== SESSION_USERS_CLASS) {
                      var expiresAt = JSON.parse(window.localStorageObj.getItem('expires_at'));
                      var refreshTime = expiresAt - TEN_MINUTES_MILLISECS;
                      var timeToRefresh = refreshTime - (new Date().getTime());
                      if (timeToRefresh > 0) {
                          scheduleTokenRenewal(timeToRefresh);
                      }
                      else {
                          // the session is going to expire within 10 minutes, so
                          // we'll refresh it immediately
                          renewLogin();
                      }
                  }
              }
          }
  
  
          function scheduleTokenRenewal(timeToRefreshMs) {
              // var refreshTime = new Date(Date.now() + timeToRefreshMs);
              // console.log('scheduling token renewal in ' +
              //             Math.round(timeToRefreshMs / 1000 / 60) + ' minutes (' +
              //             refreshTime.toString());
              nextRefreshTimer = setTimeout(renewLogin, timeToRefreshMs);
          }
  
  
          function renewLogin() {
              // console.log('renewing login');
              if (lock) {
                  lock.checkSession({}, function (err, authres) {
                      if (err) {
                          console.log('failed to renew login');
                          console.log(err);
                      }
                      else if (authres) {
                          // console.log('renewed login');
                          // console.log(authres);
                          storeToken(authres);
  
                          // schedule the next renewal!
                          var expiresInSeconds = authres.expiresIn;
                          var expiresInMillisecs = expiresInSeconds * 1000;
                          var timeToRefreshLogin = expiresInMillisecs - TEN_MINUTES_MILLISECS;
                          if (timeToRefreshLogin > 0) {
                              scheduleTokenRenewal(timeToRefreshLogin);
                          }
                      }
                  });
              }
              else {
                  console.log('Unexpected call to renewLogin');
              }
          }
  
  
          function login() {
              if (lock) {
                  lock.show({
                      languageDictionary : {
                          title: 'Log in to ML for Kids'
                      }
                  });
              }
              else {
                  console.log('Unexpected call to login');
              }
          }
  
          function reset() {
              if (lock) {
                  lock.show({
                      languageDictionary : {
                          title: 'Forgot your password?'
                      },
  
                      allowForgotPassword : true,
                      allowLogin : false,
  
                      initialScreen : 'forgotPassword'
                  });
              }
              else {
                  console.log('Unexpected call to reset');
              }
          }
  
  
          function clearAuthData() {
              if (nextRefreshTimer) {
                  clearTimeout(nextRefreshTimer);
                  nextRefreshTimer = null;
              }
  
              deferredProfile = $q.defer();
  
              window.localStorageObj.removeItem('access_token');
              window.localStorageObj.removeItem('id_token');
              window.localStorageObj.removeItem('expires_at');
              window.localStorageObj.removeItem('scopes');
              window.localStorageObj.removeItem('profile');
  
              authManager.unauthenticate();
  
              userProfile = null;
              $rootScope.isTeacher = false;
              $rootScope.isAuthenticated = false;
  
              $rootScope.$broadcast('authStateChange', 'cleared auth data');
          }
  
          function logout() {
              if (userProfile && userProfile.tenant === SESSION_USERS_CLASS && authManager.isAuthenticated()) {
                  deleteSessionUser(userProfile.user_id)
                      .then(function () {
                          clearAuthData();
                      });
              }
              else {
                  clearAuthData();
              }
          }
  
  
          function storeToken(authResult) {
              var expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
  
              var scopes = authResult.scope || REQUESTED_SCOPES || '';
  
              window.localStorageObj.setItem('access_token', authResult.accessToken);
              window.localStorageObj.setItem('id_token', authResult.idToken);
              window.localStorageObj.setItem('expires_at', expiresAt);
              window.localStorageObj.setItem('scopes', JSON.stringify(scopes));
  
              authManager.authenticate();
          }
  
          function storeProfile(profile) {
              window.localStorageObj.setItem('profile', JSON.stringify(profile));
              deferredProfile.resolve(profile);
  
              $rootScope.isTeacher = (profile.role === 'supervisor');
              $rootScope.isAuthenticated = true;
          }
  
  
          function extractAppMetadata(profile) {
              var tenant = profile['https://machinelearningforkids.co.uk/api/tenant'];
              var role = profile['https://machinelearningforkids.co.uk/api/role'];
              var user_id = profile.sub;
              profile.tenant = tenant;
              profile.role = role;
              profile.user_id = user_id;
              delete profile['https://machinelearningforkids.co.uk/api/tenant'];
              delete profile['https://machinelearningforkids.co.uk/api/role'];
              delete profile.sub;
              delete profile.picture;
              return profile;
          }
  
  
          function setupAuth() {
              if (lock) {
                  lock.interceptHash();
  
                  lock.on('authenticated', function (authResult) {
                      if (authResult && authResult.accessToken && authResult.idToken) {
                          storeToken(authResult);
  
                          lock.getUserInfo(authResult.accessToken, function (err, profile) {
                              if (err) {
                                  console.log('lock auth failure');
                                  console.log(err);
                                  return logout();
                              }
                              vm.profile = extractAppMetadata(profile);
                              storeProfile(vm.profile);
  
                              // schedule a refresh of the token a little before
                              //  it is due to expire
                              var expiresInSeconds = authResult.expiresIn;
                              var expiresInMillisecs = expiresInSeconds * 1000;
                              var timeToRefreshLogin = expiresInMillisecs - TEN_MINUTES_MILLISECS;
                              scheduleTokenRenewal(timeToRefreshLogin);
  
                              $timeout(function () {
                                  $state.go('welcome');
                                  $rootScope.$broadcast('authStateChange', 'authentication complete');
                              });
                          });
                      }
                  });
  
                  lock.on('authorization_error', function (err) {
                      if (err && err.errorDescription) {
                          if (err.errorDescription === 'Please verify your email to activate your class account') {
                              alert('Please verify your email to activate your class account\n\n' +
                                  'When you created your account, you should have been sent an email to verify your address. \n' +
                                  'Clicking on the link in that email will activate your class account.\n\n' +
                                  'Please click on the Help tab for more info');
                          }
                      }
                      $rootScope.$broadcast('authStateChange', 'authorization error');
                  });
  
                  // auth0 looks completely broken so try starting again
                  lock.on('unrecoverable_error', function (err) {
                      console.log('lock unrecoverable error');
                      console.log(err);
                      logout();
                      return window.location.reload(true);
                  });
              }
  
              // after a session has expired, tell the user what happened
              $rootScope.$on('tokenHasExpired', sessionExpired);
          }
  
  
          function sessionExpired() {
              clearAuthData();
  
              var alert = $mdDialog.alert()
                                  .title('Session expired')
                                  .textContent('Please log in again.')
                                  .ok('OK');
              $mdDialog.show(alert).finally(function () {
                  $state.go('login');
              });
          }
  
  
          function handleUnauthenticated() {
              if (hasSessionExpired()) {
                  sessionExpired();
              }
              else {
                  clearAuthData();
                  $state.go('login');
              }
          }
  
          function getProfileDeferred() {
              return deferredProfile.promise;
          }
  
          function isAuthenticated() {
              if (userProfile) {
                  // Check whether the current time is past the
                  // access token's expiry time
                  var expired = hasSessionExpired();
                  if (expired) {
                      logout();
                  }
                  return !expired;
              }
              return false;
          }
  
          function hasSessionExpired() {
              if (userProfile) {
                  // Check whether the current time is past the
                  // access token's expiry time
                  var expiresAt = JSON.parse(window.localStorageObj.getItem('expires_at'));
                  var expired = (new Date().getTime() > expiresAt);
                  return expired;
              }
              return false;
          }
  
  
  
  
          function confirmLocalStorage() {
              // some browsers allow localStorage to be disabled
              try {
                  window.localStorageObj = window.localStorage || {};
              }
              catch (err) {
                  window.localStorageObj = {};
              }
  
              // Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem
              // throw QuotaExceededError. If it looks like localStorage isn't working, we use a local object
              if (typeof window.localStorageObj === 'object') {
                  try {
                      window.localStorageObj.setItem('confirmLocalStorage', 1);
                      window.localStorageObj.removeItem('confirmLocalStorage');
                  }
                  catch (e) {
                      window._tempLocalStorage = {};
                      window.localStorageObj.setItem = function (key, val) {
                          window._tempLocalStorage[key] = val;
                      };
                      window.localStorageObj.getItem = function (key) {
                          return window._tempLocalStorage[key];
                      };
                      window.localStorageObj.removeItem = function (key) {
                          delete window._tempLocalStorage[key];
                      };
                  }
              }
          }
  
  
  
  
  
  
  
          function switchToSessionUser(userinfo) {
              // clear out any existing user/auth info
              logout();
  
              window.localStorageObj.setItem('access_token', userinfo.token);
              window.localStorageObj.setItem('id_token', userinfo.jwt);
  
              var expiryTime = JSON.stringify(new Date(userinfo.sessionExpiry).getTime());
              window.localStorageObj.setItem('expires_at', expiryTime);
  
              window.localStorageObj.setItem('scopes', 'openid email');
  
              userProfile = {
                  tenant : SESSION_USERS_CLASS,
                  role : 'student',
                  user_id : userinfo.id
              };
  
              window.localStorageObj.setItem('profile', JSON.stringify(userProfile));
              deferredProfile.resolve(userProfile);
  
              $rootScope.isAuthenticated = true;
              $rootScope.isTeacher = false;
  
              $rootScope.$broadcast('authStateChange', 'switched to session user');
          }
  
  
  
          function createSessionUser() {
              return $http.post('/api/sessionusers')
                  .then(function (resp) {
                      var sessionuser = resp.data;
  
                      switchToSessionUser(sessionuser);
  
                      return sessionuser;
                  });
          }
  
          function deleteSessionUser(userid) {
              return $http.delete('/api/classes/' + SESSION_USERS_CLASS + '/sessionusers/' + userid)
                  .catch(function (err) {
                      console.log(err);
                  });
          }
  
  
          function parseUrlParams(input) {
              var params = {};
              input.split('&').forEach(function (str) {
                  var pair = str.split('=');
                  params[pair[0]] = pair[1];
              });
              return params;
          }
  
  
          function checkForAuthMessagesInUrl() {
              var paramStr = $window.location.search;
              if (paramStr &&
                  paramStr[0] === '?')
              {
                  var params = parseUrlParams(paramStr.substr(1));
  
                  if (params.message === 'Your%20email%20was%20verified.%20You%20can%20continue%20using%20the%20application.')
                  {
                      var alert = $mdDialog.alert()
                                      .title('Welcome to Machine Learning for Kids')
                                      .textContent('Your email address has been verified.')
                                      .ok('OK');
                      $mdDialog.show(alert).finally(function () {
                          window.location = '/';
                      });
                  }
              }
          }
  
  
  
          return {
              login : login,
              reset : reset,
              logout : logout,
  
              setupAuth : setupAuth,
              getProfileDeferred : getProfileDeferred,
              isAuthenticated : isAuthenticated,
  
              handleUnauthenticated : handleUnauthenticated,
  
              createSessionUser : createSessionUser,
  
              checkForAuthMessagesInUrl : checkForAuthMessagesInUrl
          };
      }
  })();
  
  (function () {
  
      angular
          .module('app')
          .controller('DatasetsController', DatasetsController);
  
          DatasetsController.$inject = [
              'authService',
              'projectsService',
              '$state', '$translate', '$mdDialog', '$stateParams'
          ];
  
  
      function DatasetsController(authService, projectsService, $state, $translate, $mdDialog, $stateParams) {
  
          var vm = this;
          vm.authService = authService;
  
          vm.creating = false;
          vm.loading = true;
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
  
  
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
  
              if (errObj &&
                  status === 403 &&
                  errObj.error === 'Support for images projects is not enabled for your class' &&
                  vm.profile.tenant === 'session-users')
              {
                  errObj.message = 'You can\'t train machine learning models to recognise images with "Try it now". ' +
                                   'You will be able to create images projects if you login with a regular account. ' +
                                   'See the "Help" page for more details about the differences between creating an account and using "Try it now".';
              }
  
              vm[type].push({
                  alertid : alertId++,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
          }
  
          vm.datasets = [];
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  $translate([
                      'DATASETS.DATA.TITANIC.TITLE', 'DATASETS.DATA.TITANIC.SUMMARY', 'DATASETS.DATA.TITANIC.DESCRIPTION', 'DATASETS.DATA.TITANIC.DETAILS',
                      'DATASETS.DATA.UKNEWSPAPERS.TITLE', 'DATASETS.DATA.UKNEWSPAPERS.SUMMARY', 'DATASETS.DATA.UKNEWSPAPERS.DESCRIPTION', 'DATASETS.DATA.UKNEWSPAPERS.DETAILS',
                      'DATASETS.DATA.CATSANDDOGS.TITLE', 'DATASETS.DATA.CATSANDDOGS.SUMMARY', 'DATASETS.DATA.CATSANDDOGS.DESCRIPTION', 'DATASETS.DATA.CATSANDDOGS.DETAILS',
                      'DATASETS.DATA.PHISHING.TITLE', 'DATASETS.DATA.PHISHING.SUMMARY', 'DATASETS.DATA.PHISHING.DESCRIPTION', 'DATASETS.DATA.PHISHING.DETAILS'
                  ]).then(function (translations) {
                      vm.datasets = [
                          {
                              id: 'titanic',
                              title: translations['DATASETS.DATA.TITANIC.TITLE'],
                              summary: translations['DATASETS.DATA.TITANIC.SUMMARY'],
                              description: translations['DATASETS.DATA.TITANIC.DESCRIPTION'],
                              details: translations['DATASETS.DATA.TITANIC.DETAILS'],
                              type: 'numbers',
                              image: 'static/images/dataset-titanic.png'
                          },
                          {
                              id: 'uk-newspaper-headlines',
                              title: translations['DATASETS.DATA.UKNEWSPAPERS.TITLE'],
                              summary: translations['DATASETS.DATA.UKNEWSPAPERS.SUMMARY'],
                              description: translations['DATASETS.DATA.UKNEWSPAPERS.DESCRIPTION'],
                              details: translations['DATASETS.DATA.UKNEWSPAPERS.DETAILS'],
                              type: 'text',
                              image: 'static/images/dataset-ukheadlines.png'
                          },
                          {
                              id: 'cats-and-dogs',
                              title: translations['DATASETS.DATA.CATSANDDOGS.TITLE'],
                              summary: translations['DATASETS.DATA.CATSANDDOGS.SUMMARY'],
                              description: translations['DATASETS.DATA.CATSANDDOGS.DESCRIPTION'],
                              details: translations['DATASETS.DATA.CATSANDDOGS.DETAILS'],
                              type: 'images',
                              image: 'static/images/dataset-catsanddogs.png'
                          },
                          {
                              id: 'phishing',
                              title: translations['DATASETS.DATA.PHISHING.TITLE'],
                              summary: translations['DATASETS.DATA.PHISHING.SUMMARY'],
                              description: translations['DATASETS.DATA.PHISHING.DESCRIPTION'],
                              details: translations['DATASETS.DATA.PHISHING.DETAILS'],
                              type: 'numbers',
                              image: 'static/images/dataset-phishing.png'
                          }
                      ];
  
                      vm.loading = false;
                  });
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data);
              });
  
  
          vm.displayDataset = function (ev, dataset) {
              if (vm.creating) {
                  return;
              }
  
              $mdDialog.show({
                  locals : {
                      dataset : dataset
                  },
                  controller : function ($scope, locals) {
                      $scope.dataset = locals.dataset;
                      $scope.hide = function() {
                          $mdDialog.hide();
                      };
                      $scope.confirm = function() {
                          $mdDialog.hide($scope.dataset);
                      };
                      $scope.cancel = function() {
                          $mdDialog.cancel();
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/datasets/dataset.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (datasetToImport) {
                      vm.importProject(datasetToImport);
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
  
  
          vm.importProject = function (dataset) {
              if (vm.creating) {
                  return;
              }
  
              vm.creating = true;
  
              projectsService.createProject({ type : dataset.type, dataset : dataset.id },
                                            vm.profile.user_id,
                                            vm.profile.tenant)
                  .then(function () {
                      $state.go('projects');
                  })
                  .catch(function (err) {
                      displayAlert('errors', err.status, err.data);
  
                      vm.creating = false;
                  });
          };
  
  
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('ModelDescribeController', ModelDescribeController);
  
          ModelDescribeController.$inject = [
              'authService', 'projectsService', 'trainingService',
              '$stateParams', '$scope', '$timeout', '$interval', '$document'
          ];
  
      function ModelDescribeController(authService, projectsService, trainingService, $stateParams, $scope, $timeout, $interval, $document) {
          var vm = this;
          vm.authService = authService;
  
  
          $scope.loading = true;
  
          $scope.projectId = $stateParams.projectId;
          $scope.userId = $stateParams.userId;
          $scope.modelId = $stateParams.modelId;
  
          $scope.modelinfo = undefined;
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
  
              // create alert and display it
              var newId = alertId++;
              var newAlert = {
                  code : errObj.code,
                  alertid : newId,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              };
              vm[type].push(newAlert);
  
              return newId;
          }
  
          function scrollToNewItem(itemId) {
              $timeout(function () {
                  var newItem = document.getElementById(itemId);
                  $document.duScrollToElementAnimated(angular.element(newItem));
              }, 0);
          }
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  return projectsService.getProject($scope.projectId, $scope.userId, profile.tenant);
              })
              .then(function (project) {
                  $scope.project = project;
                  return trainingService.getModels($scope.projectId, $scope.userId, vm.profile.tenant);
              })
              .then(function (models) {
                  if (models && models.length > 0 && models[0].status === 'Available') {
                      return trainingService.getModel($scope.project.id, $scope.userId, vm.profile.tenant, $scope.modelId, models[0].updated);
                  }
                  else {
                      var errId = displayAlert('errors', 400, { message : 'Model not ready to be described' });
                      scrollToNewItem('errors' + errId);
                      $scope.loading = false;
                  }
              })
              .then(function (modelinfo) {
                  $scope.loading = false;
                  if (modelinfo) {
                      $scope.modelinfo = {
                          nodes : {},
                          vocabulary : modelinfo.vocabulary
                      };
                      initializeVisualisation(modelinfo.svg);
                      prepareDecisionTreeGraph(modelinfo.dot);
  
                      return projectsService.getFields($scope.projectId, $scope.userId, vm.profile.tenant);
                  }
              })
              .then(function (fields) {
                  if (fields) {
                      $scope.project.fields = fields;
                  }
              })
              .catch(function (err) {
                  var errId = displayAlert('errors', err.status, err.data);
                  scrollToNewItem('errors' + errId);
                  $scope.loading = false;
              });
  
  
          //-------------------------------------------------------------------------------
          // Adding the decision tree graphic to the page
          //-------------------------------------------------------------------------------
  
          function initializeVisualisation(svgdata) {
              $timeout(function () {
                  // prepare somewhere to put the decision tree graphic
                  var svgcontainer = document.createElement("div");
                  svgcontainer.id = 'mlforkidsmodelvizimg';
                  svgcontainer.innerHTML = svgdata;
  
                  // find where we need to add the visualization in the component
                  var svghost = document.getElementById('mlforkidsmodelvizimghost');
                  svghost.appendChild(svgcontainer);
  
                  // modify the visualization to add some custom styles needed for highlighting
                  var svgroot = svgcontainer.getElementsByTagName('svg')[0];
                  svgroot.removeAttribute('width');
                  svgroot.removeAttribute('height');
                  var styleElement = document.createElement("style");
                  styleElement.textContent = ".nothighlighted { opacity: 0.25; } .highlighted { opacity: 1; } .highlighted.node path { stroke-width: 3; }";
                  svgroot.insertBefore(styleElement, svgroot.firstChild);
              }, 0);
          }
  
  
  
          //-------------------------------------------------------------------------------
          // Processing the graph to build up info needed to highlight nodes
          //-------------------------------------------------------------------------------
  
          // parse the description of the test that graphviz puts into each node in the tree
          var SEGMENT_REGEX = /^(.*) ([<=]{1,2}) (-?[0-9.]+)$/;
          function getTestInfo(test) {
              var segmentChunks = SEGMENT_REGEX.exec(test);
              if (segmentChunks) {
                  return {
                      field : segmentChunks[1],
                      op : segmentChunks[2],
                      threshold : segmentChunks[3]
                  };
              }
              else {
                  console.log('unexpected test syntax', test);
              }
          }
  
          // parse the dot file to identify the edges between nodes in the tree
          var GRAPHVIZ_NODE_REGEX = /^([0-9]+) \[fillcolor="#[a-z0-9]{6}", label="((.*?)\\n.*)"];$/;
          var GRAPHVIZ_EDGE_REGEX = /^([0-9]+) -> ([0-9]+).*;$/;
          function prepareDecisionTreeGraph(dotfile) {
              var edgeid = 1;
  
              var dotfilelines = dotfile.split('\n');
              for (var i = 0; i < dotfilelines.length; i++) {
                  var line = dotfilelines[i];
  
                  var nodeTestMatches = GRAPHVIZ_NODE_REGEX.exec(line);
                  if (nodeTestMatches) {
                      var nodeid = nodeTestMatches[1];
                      var nodeattrs = nodeTestMatches[2].split('\\n');
                      var nodetest = nodeTestMatches[3];
  
                      $scope.modelinfo.nodes[nodeid] = { id : nodeid, children : [], edges : [] };
                      if (nodeattrs.length === 4) {
                          $scope.modelinfo.nodes[nodeid].test = getTestInfo(nodetest);
                      }
                  }
                  else {
                      var edgeTestMatches = GRAPHVIZ_EDGE_REGEX.exec(line);
                      if (edgeTestMatches) {
                          var left = edgeTestMatches[1];
                          var right = edgeTestMatches[2];
                          $scope.modelinfo.nodes[left].children.push(right);
  
  
                          var nextedge = edgeid++;
                          $scope.modelinfo.nodes[left].edges.push(nextedge);
                      }
                  }
              }
          }
  
  
          // identify edges and nodes in the tree for a given set of input values
          function identifyTreeRoute(rawdata) {
              var answers = {};
  
  			for (var featureidx = 0; featureidx < $scope.modelinfo.vocabulary.length; featureidx++) {
                  var feature = $scope.modelinfo.vocabulary[featureidx];
  
  				var split = feature.indexOf('=');
  				if (split === -1) {
  					answers[feature] = rawdata[feature];
  				}
  				else {
  					var field = feature.substr(0, split);
  					var option = feature.substr(split + 1);
  
  					answers[feature] = (option === rawdata[field]) ? 1 : 0;
  				}
              }
  
              var itemsToHighlight = [];
  
  			var nextnode = $scope.modelinfo.nodes[0];
  			while (nextnode) {
  				itemsToHighlight.push('node' + (parseInt(nextnode.id) + 1));
  
  				if (nextnode.test) {
  					var test = nextnode.test;
  
  					var testValue = answers[test.field];
  
  					var testPass = false;
  					if (test.op === '<=') {
  						testPass = (testValue <= test.threshold);
  					}
  					else if (test.op === '=') {
  						testPass = (testValue === test.threshold);
  					}
  
  					var nextnodeid = testPass ? nextnode.children[0] : nextnode.children[1];
  					itemsToHighlight.push('edge' + (testPass ? nextnode.edges[0] : nextnode.edges[1]));
  					nextnode = $scope.modelinfo.nodes[nextnodeid];
  				}
  				else {
  					nextnode = undefined;
  				}
              }
  
              return itemsToHighlight;
          }
  
  
          // make sure that numeric values are treated as numbers, not strings
          function cleanupTestData(rawdata) {
              for (var i = 0; i < $scope.project.fields.length; i++) {
                  var field = $scope.project.fields[i];
                  if (field.type === 'number') {
                      rawdata[field.name] = Number(rawdata[field.name]);
                  }
              }
              return rawdata;
          }
  
  
  
          //-------------------------------------------------------------------------------
          // Highlighting nodes in the tree
          //-------------------------------------------------------------------------------
  
          $scope.testformData = {};
  
          vm.resetTree = function () {
              // remove custom highlighting classes from all edges and nodes
              var mysvg = document.getElementById('mlforkidsmodelvizimg');
              var things = mysvg.querySelectorAll('.node,.edge');
              for (var i = 0; i < things.length; i++) {
                  var thing = things[i];
                  thing.classList.remove("highlighted");
                  thing.classList.remove("nothighlighted");
              }
          };
  
          vm.highlight = function (rawdata) {
              // clear any existing highlighting classes
              vm.resetTree();
  
              // identify the edges and nodes to highlight
              var itemsToHighlight = identifyTreeRoute(cleanupTestData(rawdata));
  
              // add highlighting classes to edges and nodes
              var mysvg = document.getElementById('mlforkidsmodelvizimg');
  			var things = mysvg.querySelectorAll('.node,.edge');
  			for (var i = 0; i < things.length; i++) {
  				var thing = things[i];
  				if (itemsToHighlight.indexOf(thing.id) >= 0) {
  					thing.classList.add("highlighted");
  				}
  				else {
  					thing.classList.add("nothighlighted");
  				}
  			}
          };
  
  
  
          //-------------------------------------------------------------------------------
          // Moving the decision tree graphic around
          //-------------------------------------------------------------------------------
  
          var operation;
  
          vm.stop = function() {
              if (operation) {
                  $interval.cancel(operation);
                  clearInterval(operation);
                  operation = undefined;
              }
          };
  
          var REPEAT_INTERVAL = 50;
          var SCROLL_PIXELS = 20;
          var scale = 100;
  
  
          vm.grow = function() {
              vm.stop();
              var image = document.getElementById("mlforkidsmodelvizimg");
              operation = $interval(function () {
                  scale += 10;
                  image.style["width"] = scale + "%";
              }, REPEAT_INTERVAL);
          };
  
          vm.shrink = function() {
              vm.stop();
              var image = document.getElementById("mlforkidsmodelvizimg");
              operation = $interval(function () {
                  if (scale > 100) {
                      scale -= 10;
                      image.style["width"] = scale + "%";
                  }
              }, REPEAT_INTERVAL);
          };
  
          vm.goleft = function() {
              vm.stop();
              var container = document.getElementById("mlforkidsmodelvizimghost");
              operation = $interval(function () {
                  container.scrollLeft += SCROLL_PIXELS;
              }, REPEAT_INTERVAL);
          };
          vm.goright = function() {
              vm.stop();
              var container = document.getElementById("mlforkidsmodelvizimghost");
              operation = $interval(function () {
                  container.scrollLeft -= SCROLL_PIXELS;
              }, REPEAT_INTERVAL);
          };
          vm.goup = function() {
              vm.stop();
              var container = document.getElementById("mlforkidsmodelvizimghost");
              operation = $interval(function () {
                  container.scrollTop -= SCROLL_PIXELS;
              }, REPEAT_INTERVAL);
          };
          vm.godown = function() {
              vm.stop();
              var container = document.getElementById("mlforkidsmodelvizimghost");
              operation = $interval(function () {
                  container.scrollTop += SCROLL_PIXELS;
              }, REPEAT_INTERVAL);
          };
  
  
  
          //-------------------------------------------------------------------------------
  
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('LoginController', LoginController);
  
      LoginController.$inject = [
          'authService', '$location', '$stateParams',
          '$document', '$scope', '$timeout', '$state',
          '$mdDialog'
      ];
  
      function LoginController(authService, $location, $stateParams, $document, $scope, $timeout, $state, $mdDialog) {
          var vm = this;
          vm.authService = authService;
  
          vm.loginstate = 'login';
          readFocusFromUrl();
  
          vm.sendEmail = function () {
              window.location = 'mailto:dale.lane@uk.ibm.com?subject=New%20MLforKids%20class%20account&body=___PLEASE%20FILL%20IN%20THIS%20TEMPLATE___%0A%0APlease%20can%20you%20setup%20a%20new%20class%20account%20for%20my%20group.%20%0A%0AI%20need%20it%20for%20my%20group%20of%20___THIS%20MANY___%20students.%20%0A%0AI%20run%20___NAME%20OF%20SCHOOL%20%2F%20DESCRIPTION%20OF%20CODING%20GROUP___.%20%0A%0AYou%20can%20find%20us%20at%20___WEB%20ADDRESS%20FOR%20SCHOOL%20OR%20CODING%20GROUP___.%0A%0AThanks%20very%20much!%0A%0A___WHO%20I%20AM___';
          };
  
          vm.outOfOffice = function (ev) {
              var confirm = $mdDialog.confirm()
                                  .title('Happy Holidays!')
                                  .htmlContent('<div class="outofoffice">I am on vacation, so I won\'t be available to set up ' +
                                                                          'classes until I get back in the New Year. </div>' +
                                               '<div class="outofoffice">You can set up your own class while I\'m away if you ' +
                                                                          'can\'t wait until I get back, or use "Try it now" to ' +
                                                                          'use the site without registering in the meantime.</div>' +
                                               '<div class="outofoffice">-- Dale</div>')
                              // .title('I need a bit of a break, sorry!')
                              // .htmlContent('<div class="outofoffice">I originally added this idea of a "Managed" class ' +
                              //                                         'to help teachers who found the signup process too ' +
                              //                                         'complicated or time-consuming. It means teachers have ' +
                              //                                         'been able to send me an email to ask me to do it for ' +
                              //                                         'them, and for the last couple of years I\'ve been ' +
                              //                                         'happy to do it to help many schools get started.</div>' +
                              //              '<div class="outofoffice">However, in recent months, this has increased ' +
                              //                                         'so much that I\'m spending hours doing this ' +
                              //                                         'every day. It\'s the main reason why I haven\'t been ' +
                              //                                         'able to add any significant new features or fixes to this ' +
                              //                                         'site recently.</div>' +
                              //              '<div class="outofoffice">I need to take a break from this. You are free to create yourself ' +
                              //                                         'a class account during this time - the only thing that is ' +
                              //                                         'pausing is my offer to email me to ask me to do it for you.</div>' +
                              //              '<div class="outofoffice">Sorry for any difficulty this causes.</div>' +
                              //              '<div class="outofoffice">-- Dale</div>')
                              .targetEvent(ev)
                              .ok('OK');
              $mdDialog.show(confirm)
                  .then(function () {
  
                        },
                        function () {});
          };
  
          vm.deployment = $stateParams.DEPLOYMENT;
  
          vm.startTryItNowSession = function (ev) {
              $scope.failure = null;
  
              authService.createSessionUser()
                  .then(function (/* newUser */) {
                      $timeout(function () {
                          $state.go('projects');
                      });
                  })
                  .catch(function (err) {
                      var errObj = err.data;
  
  
                      $scope.failure = {
                          message : getErrorMessage(errObj),
                          status : err.status
                      };
                      $timeout(function () {
                          var newItem = document.getElementById('sessionuserfailure');
                          $document.duScrollToElementAnimated(angular.element(newItem));
                      }, 0);
                  });
          };
  
  
  
  
  
          function getErrorMessage (errObj) {
              if (errObj && errObj.error) {
                  if (errObj.error === 'Class full') {
                      return 'There are too many people currently using this feature. Sorry, but I\'m limiting numbers for now. Please try later - or create yourself an account and log on now!';
                  }
                  else {
                      return errObj.error;
                  }
              }
              if (errObj && errObj.message) {
                  return errObj.message;
              }
              return 'Unknown error';
          }
  
          function readFocusFromUrl() {
              var urlParms = $location.search();
              if (urlParms && urlParms.tab) {
                  switch (urlParms.tab) {
                      case 'login':
                      case 'reset':
                      case 'signup':
                      case 'teachersignup':
                      case 'newstudent':
                      case 'whyregister':
                          vm.loginstate = urlParms.tab;
                          break;
                  }
              }
          }
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('MakesController', MakesController);
  
      MakesController.$inject = [
          'authService', '$stateParams', '$scope', 'projectsService'
      ];
  
      function MakesController(authService, $stateParams, $scope, projectsService) {
  
          var vm = this;
          vm.authService = authService;
  
          $scope.projectId = $stateParams.projectId;
          $scope.userId = $stateParams.userId;
  
          $scope.loading = true;
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  return projectsService.getProject($scope.projectId, $scope.userId, profile.tenant);
              })
              .then(function (project) {
                  $scope.project = project;
                  $scope.loading = false;
              })
              .catch(function (err) {
                  $scope.loading = false;
                  $scope.failure = {
                      message : err.data.error,
                      status : err.status
                  };
              });
      }
  }());
  
  angular.module('app')
      .directive('mlcanvas', function () {
          return {
              template: '<div class="mlcanvas">' +
                          '<canvas width="500" height="300" ' +
                          '   ng-mousedown="handleMouseDown($event)"' +
                          '   ng-mousemove="handleMouseMove($event)"' +
                          '    ></canvas>' +
                          '<div class="canvastools">' +
                          '    <div class="toolslabel">Tools</div>' +
                          '    <div class="btn-group" data-toggle="buttons">' +
                          '        <label class="btn btn-default" ng-class="{ \'active\' : canvastool === \'tooldraw\' }">' +
                          '            <input type="radio" value="tooldraw" name="drawtooloptions" ng-model="canvastool"> Draw' +
                          '        </label>' +
                          '        <label class="btn btn-default" ng-class="{ \'active\' : canvastool === \'toolerase\' }">' +
                          '            <input type="radio" value="toolerase" name="drawtooloptions" ng-model="canvastool"> Erase' +
                          '        </label>' +
                          '    </div>' +
                          '    <div class="btn btn-default" ng-click="reset()">Reset</div> ' +
                          '</div>' +
                        '</div>',
  
              restrict: 'E',
  
              link: function ($scope, element, attrs, controller, transcludeFn) {
  
                  $scope.canvastool = 'tooldraw';
  
                  var mousex = 0;
                  var mousey = 0;
  
                  var last_mousex = 0;
                  var last_mousey = 0;
  
                  $scope.canvas = element.find('canvas')[0];
                  var ctx = $scope.canvas.getContext('2d');
  
                  function isMouseButtonDown(mouseevt) {
                      if (mouseevt.buttons !== undefined) {
                          return mouseevt.buttons === 1;
                      }
                      // for Safari support, which doesn't support
                      //  'which' for mouseevnets
                      return mouseevt.which === 1;
                  }
  
                  $scope.handleMouseDown = function(e) {
                      last_mousex = mousex = e.offsetX;
                      last_mousey = mousey = e.offsetY;
                  };
                  $scope.handleMouseMove = function handleMouseMove(e) {
                      mousex = e.offsetX;
                      mousey = e.offsetY;
  
                      if (isMouseButtonDown(e)) {
                          ctx.beginPath();
                          ctx.globalCompositeOperation = 'source-over';
                          if ($scope.canvastool == 'tooldraw') {
                              ctx.strokeStyle = 'black';
                              ctx.lineWidth = 5;
                          }
                          else {
                              ctx.strokeStyle = 'white';
                              ctx.lineWidth = 20;
                          }
                          ctx.moveTo(last_mousex, last_mousey);
                          ctx.lineTo(mousex, mousey);
  
                          ctx.lineJoin = ctx.lineCap = 'round';
                          ctx.stroke();
                      }
                      last_mousex = mousex;
                      last_mousey = mousey;
                  };
  
                  $scope.reset = function reset() {
                      ctx.fillStyle = "rgb(255,255,255)";
                      ctx.fillRect(0,0,500,300);
                  };
  
                  $scope.reset();
              }
          };
      });
  
  
  
  
  
  (function () {
  
      angular
          .module('app')
          .controller('ProjectController', ProjectController);
  
      ProjectController.$inject = [
          'authService', 'projectsService',
          '$stateParams', '$scope'
      ];
  
      function ProjectController(authService, projectsService, $stateParams, $scope) {
          var vm = this;
          vm.authService = authService;
  
          $scope.projectId = $stateParams.projectId;
          $scope.userId = $stateParams.userId;
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  return projectsService.getProject($scope.projectId, $scope.userId, profile.tenant);
              })
              .then(function (project) {
                  $scope.project = project;
              })
              .catch(function (err) {
                  $scope.failure = {
                      message : err.data.error,
                      status : err.status
                  };
              });
      }
  }());
  
  angular.module('app')
      .directive('mlsoundvis', function () {
  
          return {
              template: '<canvas width="150" height="100"></canvas>',
  
              restrict: 'E',
  
              scope: {
                  spectogram : '<',
                  modelinfo : '<',
                  datatype : '<'
              },
  
              link: function ($scope, element, attrs, controller, transcludeFn) {
  
                  if ($scope.datatype !== 'sounds') {
                      return;
                  }
  
                  var min = Infinity;
                  var max = -Infinity;
                  for (var i = 0; i < $scope.spectogram.length; i++) {
                      var num = $scope.spectogram[i];
                      if (num > -Infinity) {
                          min = Math.min(num, min);
                          max = Math.max(num, max);
                      }
                  }
                  if (min >= max) {
                      console.log('Invalid data range supplied');
                      return;
                  }
  
                  var canvas = element[0].firstChild;
                  var context = canvas.getContext('2d');
                  context.clearRect(0, 0, canvas.width, canvas.height);
  
                  var fftSize = $scope.modelinfo.fftSize;
  
                  var numFrames = $scope.spectogram.length / fftSize;
  
                  var pixelsPerCol = canvas.width / numFrames;
                  var pixelsPerRow = canvas.height / fftSize;
  
                  // for each column...
                  for (i = 0; i < numFrames; ++i) {
                      var x = pixelsPerCol * i;
  
                      // get values for the column
                      var columnValues = $scope.spectogram.slice(i * fftSize, (i + 1) * fftSize);
                      if (columnValues[0] > -Infinity) {
  
                          // for each row in the column
                          for (var j = 0; j < fftSize; ++j) {
                              var y = canvas.height - (j + 1) * pixelsPerRow;
  
                              var colour = (columnValues[j] - min) / (max - min);
                              colour = Math.pow(colour, 3);
                              colour = Math.round(255 * colour);
  
                              var fillStyle = 'rgb(' + (255 - colour) + ',' + (255 - colour) + ',' + colour + ')';
                              context.fillStyle = fillStyle;
                              context.fillRect(x, y, pixelsPerCol, pixelsPerRow);
                          }
                      }
                  }
              }
          };
      });
  
  (function () {
  
      angular
          .module('app')
          .controller('ModelsController', ModelsController);
  
      ModelsController.$inject = [
          'authService',
          'projectsService', 'trainingService', 'quizService', 'soundTrainingService',
          '$stateParams',
          '$scope', '$mdDialog', '$timeout', '$interval', '$q', '$document', '$state'
      ];
  
      function ModelsController(authService, projectsService, trainingService, quizService, soundTrainingService, $stateParams, $scope, $mdDialog, $timeout, $interval, $q, $document, $state) {
  
          var vm = this;
          vm.authService = authService;
  
          // -------------------------------------------------
          //  error handling
          // -------------------------------------------------
          var THREE_MINUTES = 3 * 60 * 1000;
  
          var lastAlert = {
              code : 'ERR',
              message : 'Something went wrong',
              incidences : 0,
              firstseen : new Date().getTime()
          };
  
          function reset() {
              lastAlert = {
                  code : 'ERR',
                  message : 'Something went wrong',
                  incidences : 0,
                  firstseen : new Date().getTime()
              };
          }
          function checkForRepeatedErrors(newAlert) {
              var threeMinutesAgo = new Date().getTime() - THREE_MINUTES;
  
              var hasMatchingCode = newAlert.code ? (newAlert.code === lastAlert.code) : false;
              var hasMatchingMessage = newAlert.code ? false : (newAlert.message === lastAlert.message);
              var isRecent = (lastAlert.firstseen > threeMinutesAgo);
  
  
              if ((hasMatchingCode || hasMatchingMessage) && isRecent)
              {
                  // we've seen this error before within the last few minutes!
                  lastAlert.incidences += 1;
              }
              else {
                  // this is different to the last error, so reset the count
                  lastAlert = {
                      code : newAlert.code,
                      message : newAlert.message,
                      incidences : 1,
                      firstseen : new Date().getTime()
                  };
              }
  
              // should we give the user some extra help?
              if (lastAlert.incidences > 3){
                  var errCode = lastAlert.code;
  
                  $mdDialog.show({
                      controller : function ($scope) {
                          $scope.code = errCode;
  
                          $scope.hide = function() {
                              $mdDialog.hide();
                          };
                      },
                      templateUrl : 'static/components-' + $stateParams.VERSION + '/models/errordetail.tmpl.html'
                  })
                  .then(
                      function () {
                          // informational only. do nothing
                      },
                      function() {
                          // cancelled. do nothing
                      }
                  );
  
                  reset();
              }
          }
  
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
              else {
                  // record the error
                  console.log(errObj);
                  if (status === 500 && Sentry && Sentry.captureException) {
                      Sentry.captureException({
                          error : errObj,
                          errortype : typeof (errObj),
                          projectid : $scope.projectId
                      });
                  }
              }
  
              // create alert and display it
              var newId = alertId++;
              var newAlert = {
                  code : errObj.code,
                  alertid : newId,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              };
              vm[type].push(newAlert);
  
              // check if we're seeing this alert a lot
              checkForRepeatedErrors(newAlert);
  
              return newId;
          }
          // -------------------------------------------------
  
  
  
          $scope.loading = true;
          $scope.status = 'unknown';
          $scope.projectId = $stateParams.projectId;
          $scope.userId = $stateParams.userId;
  
          $scope.projecturls = {
              train : '/#!/mlproject/' + $stateParams.userId + '/' + $stateParams.projectId + '/training'
          };
  
          $scope.owner = true;
  
          $scope.minimumExamples = 'five';
          $scope.testformData = {};
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  return $q.all({
                      project : projectsService.getProject($scope.projectId, $scope.userId, vm.profile.tenant),
                      labels : projectsService.getLabels($scope.projectId, $scope.userId, vm.profile.tenant),
                      models : trainingService.getModels($scope.projectId, $scope.userId, vm.profile.tenant)
                  });
              })
              .then(function (values) {
                  $scope.project = values.project;
  
                  $scope.owner = (vm.profile.user_id === $scope.project.userid);
  
                  $scope.projecturls.train = '/#!/mlproject/' + $scope.project.userid + '/' + $scope.project.id + '/training';
  
                  if (values.project.type === 'images') {
                      $scope.minimumExamples = 'ten';
                  }
                  $scope.models = values.models;
                  $scope.projectSummary = generateProjectSummary();
  
                  reviewTrainingData(values.labels);
                  $scope.status = getStatus();
  
                  if ($scope.project.type === 'numbers') {
                      return projectsService.getFields($scope.projectId, $scope.userId, vm.profile.tenant);
                  }
                  if ($scope.project.type === 'sounds') {
                      $scope.listening = false;
                      return soundTrainingService.initSoundSupport($scope.project.id);
                  }
              })
              .then(function (fields) {
                  $scope.project.fields = fields;
  
                  $scope.loading = false;
              })
              .catch(function (err) {
                  var errId = displayAlert('errors', err.status, err.data ? err.data : err);
                  scrollToNewItem('errors' + errId);
              });
  
  
  
          function reviewTrainingData (labels) {
              var no_data = true;
              var insufficient_data = 0;
              var MIN = 5;
              if ($scope.project.type === 'images') {
                  MIN = 10;
              }
              else if ($scope.project.type === 'sounds') {
                  MIN = 8;
              }
              var labelslist = Object.keys(labels);
  
              $scope.trainingcounts = labelslist.map(function (label) {
                  var count = labels[label];
                  if (count > 0) {
                      no_data = false;
                  }
                  if (count < MIN) {
                      insufficient_data += 1;
                  }
                  return { label : label, count : count };
              });
  
              if (no_data) {
                  $scope.trainingdatastatus = 'no_data';
              }
              else {
                  if (insufficient_data > 1 ||
                      insufficient_data === labelslist.length ||
                      labelslist.length < 2 ||
                      ($scope.project.type === 'sounds' && insufficient_data > 0))
                  {
                      $scope.trainingdatastatus = 'insufficient_data';
                  }
                  else {
                      $scope.trainingdatastatus = 'data';
                  }
              }
          }
  
  
  
          function getStatus() {
              if (allModelsAreTraining($scope.models)) {
                  return 'training';
              }
              if (allModelsAreGood($scope.models)) {
                  return 'ready';
              }
              if ($scope.models.length === 0) {
                  return 'idle';
              }
              return 'error';
          }
  
  
          function allModelsAreTraining (models) {
              return models &&
                     models.length > 0 &&
                     !(models.some(function (model) { return model.status !== 'Training'; }));
          }
          function allModelsAreGood (models) {
              return models &&
                     models.length > 0 &&
                     !(models.some(function (model) { return model.status !== 'Available'; }));
          }
  
  
          var timer = null;
  
          function stopRefreshing() {
              if (timer) {
                  $interval.cancel(timer);
                  timer = null;
              }
          }
  
          function refreshModels () {
              if (!timer) {
                  var interval = $scope.project.type === 'sounds' ? 2000 : 30000;
  
                  timer = $interval(function () {
                      fetchModels()
                          .then(function () {
                              if ($scope.status !== 'training') {
                                  stopRefreshing();
                              }
                          });
                  }, interval);
              }
          }
  
          function allAnswersAreCorrect (answers) {
              return !(answers.some(function (answer) { return answer.selected !== answer.correct; }));
          }
  
          vm.checkQuizAnswers = function (quizQuestion) {
              // timeout is to allow the ng-model to update to reflect the
              //  user's click before evaluating the answer based on the model
              $timeout(function () {
                  $scope.answered = true;
                  $scope.answerCorrect = allAnswersAreCorrect(quizQuestion.answers);
  
                  if ($scope.answerCorrect === false) {
                      quizQuestion.answers.forEach(function (answer) {
                          answer.selected = answer.correct;
                      });
                  }
              }, 0);
          };
          vm.nextQuizQuestion = function () {
              $scope.answered = false;
              var lastQuestion = $scope.quizQuestion;
              $scope.quizQuestion = quizService.getQuestion();
  
              if ($scope.answerCorrect === false) {
                  quizService.restoreQuestion(lastQuestion);
              }
          };
  
  
          function fetchModels() {
              var modelFunction = $scope.project.type === 'sounds' ?
                                      soundTrainingService.getModels() :
                                      trainingService.getModels($scope.projectId, $scope.userId, vm.profile.tenant);
              return modelFunction.then(function (models) {
                  $scope.models = models;
                  $scope.status = getStatus();
              });
          }
  
  
  
          vm.createModel = function (ev, project) {
              // prepare the first question for displaying while
              //  the training is running
              $scope.quizQuestion = quizService.getQuestion();
  
              $scope.submittingTrainingRequest = true;
  
              if (project.type === 'sounds') {
                  soundTrainingService.newModel(project.id, $scope.userId, vm.profile.tenant)
                      .then(function (newmodel) {
                          $scope.models = [ newmodel ];
                          $scope.status = getStatus();
  
                          $scope.submittingTrainingRequest = false;
  
                          refreshModels();
                      })
                      .catch(function (err) {
                          var errId = displayAlert('errors', err.status, err.data);
                          scrollToNewItem('errors' + errId);
                      });
              }
              else {
                  trainingService.newModel(project.id, $scope.userId, vm.profile.tenant)
                      .then(function (newmodel) {
                          $scope.models = [ newmodel ];
                          $scope.status = getStatus();
  
                          $scope.submittingTrainingRequest = false;
  
                          refreshModels();
                      })
                      .catch(function (err) {
                          $scope.submittingTrainingRequest = false;
  
                          if (createModelFailedDueToDownloadFail(err)) {
                              return $mdDialog.show({
                                  controller : function ($scope) {
                                      $scope.location = err.data.location;
  
                                      $scope.hide = function() {
                                          $mdDialog.hide();
                                      };
                                      $scope.cancel = function() {
                                          $mdDialog.cancel();
                                      };
                                      $scope.confirm = function() {
                                          $mdDialog.hide(err.data.location);
                                      };
                                  },
                                  templateUrl : 'static/components-' + $stateParams.VERSION + '/models/downloadfail.tmpl.html'
                              })
                              .then(
                                  function (location) {
                                      if (location) {
                                          trainingService.deleteTrainingData(project.id, $scope.userId, vm.profile.tenant, location.imageid)
                                              .then(function() {
                                                  $state.reload();
                                              })
                                              .catch(function (e) {
                                                  var errId = displayAlert('errors', e.status, e.data);
                                                  scrollToNewItem('errors' + errId);
                                              });
                                      }
                                  },
                                  function() {
                                      // cancelled. do nothing
                                  }
                              );
                          }
                          else {
                              var errId = displayAlert('errors', err.status, err.data);
                              scrollToNewItem('errors' + errId);
                          }
                      });
              }
          };
  
  
          function createModelFailedDueToDownloadFail(err) {
              return err &&
                     err.status === 409 &&
                     err.data && err.data.code &&
                     (err.data.code === 'MLMOD12' || err.data.code === 'MLMOD13' || err.data.code === 'MLMOD14') &&
                     err.data.location && err.data.location.imageid && err.data.location.url &&
                     err.data.location.type === 'download';
          }
  
  
          vm.testModel = function (ev, form, project) {
              var testdata = { type : project.type };
  
              if (project.type === 'text') {
                  testdata.text = $scope.testformData.testquestion;
              }
              else if (project.type === 'images') {
                  testdata.image = $scope.testformData.testimageurl;
  
                  if (testdata.image &&
                      (testdata.image.substring(0, 10) === 'data:image' ||
                       testdata.image.substring(0, 5) === 'blob:'))
                  {
                      var errId = displayAlert('errors', 400, {
                          message : 'Invalid URL. Please enter the web address for a picture that you want to test your machine learning model on'
                      });
                      return scrollToNewItem('errors' + errId);
                  }
              }
              else if (project.type === 'numbers') {
                  testdata.numbers = project.fields.map(function (field) {
                      if (field.type === 'number') {
                          return parseFloat($scope.testformData[field.name]);
                      }
                      return $scope.testformData[field.name];
                  });
              }
              else if (project.type === 'sounds') {
                  return;
              }
  
              $scope.testoutput = "please wait...";
              $scope.testoutput_explanation = "";
  
              trainingService.testModel(project.id, project.type,
                                        $scope.userId, vm.profile.tenant,
                                        $scope.models[0].classifierid, $scope.models[0].credentialsid,
                                        testdata)
                  .then(function (resp) {
                      if (resp && resp.length > 0) {
                          $scope.testoutput = resp[0].class_name;
                          $scope.testoutput_explanation = "with " + Math.round(resp[0].confidence) + "% confidence";
                      }
                      else {
                          $scope.testoutput = 'Unknown';
                          $scope.testoutput_explanation = "Test value could not be recognised";
                      }
                  })
                  .catch(function (err) {
                      delete $scope.testoutput;
  
                      var errId = displayAlert('errors', err.status, err.data);
                      scrollToNewItem('errors' + errId);
                  });
          };
  
  
  
  
          vm.deleteModel = function (ev, project, model) {
              $scope.submittingDeleteRequest = true;
              var classifierid = model.classifierid;
              trainingService.deleteModel(project.id, $scope.userId, vm.profile.tenant, classifierid)
                  .then(function () {
                      $scope.models = $scope.models.filter(function (md) {
                          return md.classifierid !== classifierid;
                      });
                      $scope.status = getStatus();
  
                      if ($scope.status === 'training') {
                          refreshModels();
                      }
                      else {
                          stopRefreshing();
                      }
  
                      $scope.submittingDeleteRequest = false;
                  })
                  .catch(function (err) {
                      $scope.submittingDeleteRequest = false;
  
                      if (err.status === 404 && err.data && err.data.error === 'Not found') {
                          return $state.reload();
                      }
  
                      var errId = displayAlert('errors', err.status, err.data);
                      scrollToNewItem('errors' + errId);
                  });
          };
  
  
          vm.useWebcam = function (ev, label) {
              $scope.testformData.testimageurl = '';
  
              $mdDialog.show({
                  controller : function ($scope) {
                      $scope.channel = {};
                      $scope.webcamerror = false;
                      $scope.webcamInitComplete = false;
  
                      $scope.webcamCanvas = null;
  
                      $scope.hide = function() {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function() {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function() {
                          $mdDialog.hide(getWebcamData());
                      };
  
                      $scope.onWebcamSuccess = function () {
                          $scope.$apply(function() {
                              $scope.webcamInitComplete = true;
                          });
                      };
  
                      function displayWebcamError(err) {
                          $scope.webcamerror = err;
                          if (err && err.message) {
                              if (err.name === 'NotAllowedError') {
                                  $scope.webcamerrordetail = 'Not allowed to use the web-cam';
                              }
                              else {
                                  $scope.webcamerrordetail = err.message;
                              }
                          }
                      }
  
                      $scope.onWebcamError = function(err) {
                          $scope.webcamInitComplete = true;
  
                          try {
                              $scope.$apply(
                                  function() {
                                      displayWebcamError(err);
                                  }
                              );
                          }
                          catch (applyErr) {
                              $timeout(function () {
                                  displayWebcamError(err);
                              }, 0, false);
                          }
                      };
  
  
                      function getWebcamData() {
                          var hiddenCanvas = document.createElement('canvas');
                          hiddenCanvas.width = $scope.channel.video.width;
                          hiddenCanvas.height = $scope.channel.video.height;
  
                          var ctx = hiddenCanvas.getContext('2d');
                          ctx.drawImage($scope.channel.video,
                              0, 0,
                              $scope.channel.video.width, $scope.channel.video.height);
  
                          var imagedata = hiddenCanvas.toDataURL('image/jpeg');
                          var strippedHeaderData = imagedata.substr(imagedata.indexOf(',') + 1);
                          return strippedHeaderData;
                      };
  
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/models/webcam.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (webcamimagedata) {
                      $scope.testoutput = "please wait...";
                      $scope.testoutput_explanation = "";
  
                      trainingService.testModel($scope.project.id, $scope.project.type,
                                                $scope.userId, vm.profile.tenant,
                                                $scope.models[0].classifierid, $scope.models[0].credentialsid,
                                                { type : $scope.project.type, data : webcamimagedata })
                          .then(function (resp) {
                              if (resp && resp.length > 0) {
                                  $scope.testoutput = resp[0].class_name;
                                  $scope.testoutput_explanation = "with " + Math.round(resp[0].confidence) + "% confidence";
                              }
                              else {
                                  $scope.testoutput = 'Unknown';
                                  $scope.testoutput_explanation = "Test value could not be recognised";
                              }
                          })
                          .catch(function (err) {
                              var errId = displayAlert('errors', err.status, err.data);
                              scrollToNewItem('errors' + errId);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
  
          vm.useCanvas = function (ev) {
              $scope.testformData.testimageurl = '';
  
              $mdDialog.show({
                  controller : function ($scope) {
                      $scope.hide = function() {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function() {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function() {
                          var imagedata = $scope.canvas.toDataURL('image/jpeg');
                          var strippedHeaderData = imagedata.substr(imagedata.indexOf(',') + 1);
                          $mdDialog.hide(strippedHeaderData);
                      };
  
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/models/canvas.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (canvasimagedata) {
                      $scope.testoutput = "please wait...";
                      $scope.testoutput_explanation = "";
  
                      trainingService.testModel($scope.project.id, $scope.project.type,
                                                $scope.userId, vm.profile.tenant,
                                                $scope.models[0].classifierid, $scope.models[0].credentialsid,
                                                { type : $scope.project.type, data : canvasimagedata })
                          .then(function (resp) {
                              if (resp && resp.length > 0) {
                                  $scope.testoutput = resp[0].class_name;
                                  $scope.testoutput_explanation = "with " + Math.round(resp[0].confidence) + "% confidence";
                              }
                              else {
                                  $scope.testoutput = 'Unknown';
                                  $scope.testoutput_explanation = "Test value could not be recognised";
                              }
                          })
                          .catch(function (err) {
                              var errId = displayAlert('errors', err.status, err.data);
                              scrollToNewItem('errors' + errId);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
          vm.startListening = function () {
              console.log('startListening');
              if (!$scope.listening) {
                  $scope.listening = true;
                  soundTrainingService.startTest(function (resp) {
                      $scope.$apply(
                          function() {
                              $scope.testoutput = resp[0].class_name;
                              $scope.testoutput_explanation = "with " + Math.round(resp[0].confidence) + "% confidence";
                          });
                  });
              }
          };
          vm.stopListening = function () {
              if ($scope.listening) {
                  $scope.listening = false;
                  soundTrainingService.stopTest()
                      .then(function () {
                          $scope.$apply(
                              function() {
                                  delete $scope.testoutput;
                                  delete $scope.testoutput_explanation;
                              });
                      })
                      .catch(function (err) {
                          console.log('Unable to stop listening', err);
                      });
              }
          };
  
  
  
  
          $scope.$on("$destroy", stopRefreshing);
  
  
          function generateProjectSummary() {
              if ($scope.project.labels.length > 0) {
                  var summary = '';
                  switch ($scope.project.labels.length) {
                      case 1:
                          summary = $scope.project.labels[0];
                          break;
                      case 2:
                          summary = $scope.project.labels[0] + ' or ' + $scope.project.labels[1];
                          break;
                      case 3:
                          summary = $scope.project.labels[0] + ', ' +
                                      $scope.project.labels[1] + ' or ' +
                                      $scope.project.labels[2];
                          break;
                      default:
                          summary = $scope.project.labels[0] + ', ' +
                                      $scope.project.labels[1] + ' or ' +
                                      ($scope.project.labels.length - 2) + ' other classes';
                          break;
                  }
                  return summary;
              }
          }
  
  
  
          function scrollToNewItem(itemId) {
              $timeout(function () {
                  var newItem = document.getElementById(itemId);
                  $document.duScrollToElementAnimated(angular.element(newItem));
              }, 0);
          }
  
  
      }
  
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('NewProjectController', NewProjectController);
  
      NewProjectController.$inject = [
          'authService',
          'projectsService',
          '$state', '$rootScope'
      ];
  
  
      function NewProjectController(authService, projectsService, $state, $rootScope) {
  
          var vm = this;
          vm.authService = authService;
  
          vm.creating = false;
  
          vm.fields = [];
          vm.focused = $rootScope.isTeacher ? 'crowdsourced' : 'name';
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
  
              if (errObj &&
                  status === 403 &&
                  errObj.error === 'Support for images projects is not enabled for your class' &&
                  vm.profile.tenant === 'session-users')
              {
                  errObj.message = 'You can\'t train machine learning models to recognise images with "Try it now". ' +
                                   'You will be able to create images projects if you login with a regular account. ' +
                                   'See the "Help" page for more details about the differences between creating an account and using "Try it now".';
              }
  
              vm[type].push({
                  alertid : alertId++,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
          }
  
          var MIN_CHOICE_LENGTH = 1;
          var MAX_CHOICE_LENGTH = 9;
          var MIN_NUM_CHOICES = 2;
          var MAX_NUM_CHOICES = 5;
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data);
              });
  
          var IS_VALID_CHOICE = /^[^0-9\-.,][^,]*$/;
  
          function containsInvalidChoice(choices) {
              return choices.some(function (choice) {
                  return (IS_VALID_CHOICE.test(choice) === false) ||
                          choice.length > MAX_CHOICE_LENGTH ||
                          choice.length < MIN_CHOICE_LENGTH;
              });
          }
  
          vm.isInvalid = function (type) {
              if (type === 'numbers') {
                  if (vm.fields.length < 1 || vm.fields.length > 10) {
                      return true;
                  }
                  for (var i = 0; i < vm.fields.length; i++) {
                      if (vm.fields[i].type === 'multichoice')
                      {
                          if (vm.fields[i].choices.length < MIN_NUM_CHOICES ||
                              vm.fields[i].choices.length > MAX_NUM_CHOICES)
                          {
                              return true;
                          }
  
                          if (containsInvalidChoice(vm.fields[i].choices)) {
                              return true;
                          }
                      }
                  }
              }
              return false;
          };
  
          vm.addFieldChoice = function (choice, field) {
              if (choice) {
                  var newChoice = choice.trim();
                  if (newChoice.length > 0 &&
                      newChoice.length <= MAX_CHOICE_LENGTH &&
                      field.choices.indexOf(newChoice) === -1)
                  {
                      field.choices.push(newChoice);
                      return true;
                  }
              }
              return false;
          };
  
          vm.confirm = function (projectSpec) {
              vm.creating = true;
  
              if (projectSpec.type !== 'numbers') {
                  delete projectSpec.fields;
              }
  
              projectsService.createProject(projectSpec, vm.profile.user_id, vm.profile.tenant)
                  .then(function () {
                      $state.go('projects');
                  })
                  .catch(function (err) {
                      displayAlert('errors', err.status, err.data);
  
                      vm.creating = false;
                  });
          };
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('ProjectsController', ProjectsController);
  
      ProjectsController.$inject = [
          'authService',
          'projectsService',
          '$mdDialog'
      ];
  
      function ProjectsController(authService, projectsService, $mdDialog) {
  
          var vm = this;
          vm.authService = authService;
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
              vm[type].push({
                  alertid : alertId++,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
          }
  
  
          function refreshProjectsList(profile) {
              projectsService.getProjects(profile)
                  .then(function (projects) {
                      vm.projects = projects;
  
                      for (var i = 0; i < vm.projects.length; i++) {
                          var project = vm.projects[i];
  
                          var labels = project.type === 'sounds' ?
                              project.labels.filter(function (label) {
                                  return label !== '_background_noise_';
                              }) :
                              project.labels;
  
                          if (labels.length > 0) {
                              var summary = '';
                              switch (labels.length) {
                                  case 1:
                                      summary = labels[0];
                                      break;
                                  case 2:
                                      summary = labels[0] + ' or ' + labels[1];
                                      break;
                                  case 3:
                                      summary = labels[0] + ', ' +
                                                labels[1] + ' or ' +
                                                labels[2];
                                      break;
                                  default:
                                      summary = labels[0] + ', ' +
                                                labels[1] + ' or ' +
                                                (labels.length - 2) + ' other classes';
                                      break;
                              }
                              project.labelsSummary = summary;
                          }
                      }
                  })
                  .catch(function (err) {
                      displayAlert('errors', err.status, err.data);
                  });
          }
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  refreshProjectsList(profile);
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data);
              });
  
  
          vm.deleteProject = function (ev, project) {
              var confirm = $mdDialog.confirm()
                  .title('Are you sure?')
                  .textContent('Do you want to delete ' + project.name + '? (This cannot be undone)')
                  .ariaLabel('Confirm')
                  .targetEvent(ev)
                  .ok('Yes')
                  .cancel('No');
  
              $mdDialog.show(confirm).then(
                  function() {
                      project.isPlaceholder = true;
                      projectsService.deleteProject(project, vm.profile.user_id, vm.profile.tenant)
                          .then(function () {
                              var idx = findProjectIndex(project.id);
                              if (idx !== -1) {
                                  vm.projects.splice(idx, 1);
                              }
                              else {
                                  refreshProjectsList(vm.profile);
                              }
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
          vm.displayCrowdSourcedInfo = function (ev) {
              ev.stopPropagation();
              ev.preventDefault();
  
              $mdDialog.show(
                  $mdDialog.alert()
                    .clickOutsideToClose(true)
                    .title('Crowd-sourced project')
                    .textContent('This is a whole-class project. All the students in your class are able to access it and contribute training data to it.')
                    .ariaLabel('Crowd-sourced project')
                    .ok('Got it!')
                    .targetEvent(ev)
                );
          };
  
          function findProjectIndex(id) {
              var len = vm.projects.length;
              for (var i = 0; i < len; i++) {
                  if (vm.projects[i].id === id) {
                      return i;
                  }
              }
              return -1;
          }
  
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .service('projectsService', projectsService);
  
      projectsService.$inject = [
          '$q', '$http'
      ];
  
      function projectsService($q, $http) {
  
          function getClassProjects(profile) {
              return $http.get('/api/classes/' + profile.tenant + '/projects')
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function getProjects(profile) {
              return $http.get('/api/classes/' + profile.tenant + '/students/' + profile.user_id + '/projects')
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function getProject(projectid, userid, tenant) {
              return $http.get('/api/classes/' + tenant + '/students/' + userid + '/projects/' + projectid)
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function getFields(projectid, userid, tenant) {
              return $http.get('/api/classes/' + tenant + '/students/' + userid + '/projects/' + projectid + '/fields')
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function getLabels(projectid, userid, tenant) {
              return $http.get('/api/classes/' + tenant + '/students/' + userid + '/projects/' + projectid + '/labels')
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function addLabelToProject(projectid, userid, tenant, newlabel) {
              return $http.patch('/api/classes/' + tenant + '/students/' + userid + '/projects/' + projectid, [
                      {
                          op : 'add',
                          path : '/labels',
                          value : newlabel
                      }
                  ])
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function removeLabelFromProject(projectid, userid, tenant, label) {
              return $http.patch('/api/classes/' + tenant + '/students/' + userid + '/projects/' + projectid, [
                      {
                          op : 'remove',
                          path : '/labels',
                          value : label
                      }
                  ])
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function deleteProject(project, userid, tenant) {
              return $http.delete('/api/classes/' + tenant + '/students/' + userid + '/projects/' + project.id);
          }
  
          function createProject(projectAttrs, userid, tenant) {
              return $http.post('/api/classes/' + tenant + '/students/' + userid + '/projects', projectAttrs)
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
  
          return {
              getProject : getProject,
              getProjects : getProjects,
              getClassProjects : getClassProjects,
  
              deleteProject : deleteProject,
              createProject : createProject,
  
              getFields : getFields,
              getLabels : getLabels,
  
              addLabelToProject : addLabelToProject,
              removeLabelFromProject : removeLabelFromProject
          };
      }
  })();
  
      (function () {
  
          angular
              .module('app')
              .controller('PythonController', PythonController);
  
          PythonController.$inject = [
              'authService', 'projectsService', 'scratchkeysService',
              '$stateParams', '$scope'
          ];
  
          function PythonController(authService, projectsService, scratchkeysService, $stateParams, $scope) {
  
              var vm = this;
              vm.authService = authService;
  
              $scope.projectId = $stateParams.projectId;
              $scope.userId = $stateParams.userId;
  
              $scope.functionType = 'classify';
  
              $scope.testsource = 'local';
              $scope.testdata = {
                  text      : 'The text that you want to test',
                  storetext : 'The text that you want to store',
                  imagefile : 'my-test-image.jpg',
                  imageurl  : 'https://www.site-on-the-internet.com/image.jpg',
                  fields    : [],
                  label     : 'label'
              };
  
              $scope.setSource = function (source) {
                  $scope.testsource = source;
              };
              $scope.setFunctionType = function (type) {
                  $scope.functionType = type;
              };
  
              authService.getProfileDeferred()
                  .then(function (profile) {
                      vm.profile = profile;
  
                      return projectsService.getProject($scope.projectId, $scope.userId, profile.tenant);
                  })
                  .then(function (project) {
                      $scope.project = project;
                      if (project.labels && project.labels.length > 0) {
                          $scope.testdata.label = project.labels[0];
                      }
  
                      if (project.type === 'numbers') {
                          return projectsService.getFields($scope.projectId, $scope.userId, vm.profile.tenant);
                      }
                      else {
                          return;
                      }
                  })
                  .then(function (fields) {
                      $scope.fields = fields;
  
                      return scratchkeysService.getScratchKeys($scope.project.id, $scope.userId, vm.profile.tenant);
                  })
                  .then(function (resp) {
                      if (resp) {
                          $scope.scratchkey = resp[0];
  
                          if (!$scope.scratchkey.model && $scope.project.type === 'text') {
                              $scope.functionType = 'store';
                          }
                      }
                  })
                  .catch(function (err) {
                      $scope.failure = {
                          message : err.data.error,
                          status : err.status
                      };
                  });
          }
      }());
  
  (function () {
  
      angular
          .module('app')
          .service('quizService', quizService);
  
      quizService.$inject = ['$translate'];
  
  
      function quizService($translate) {
  
          var allQuestions = [];
          $translate([
              'MODELS.QUIZ.QUESTION_1.QUESTION',
                  'MODELS.QUIZ.QUESTION_1.ANSWER_1', 'MODELS.QUIZ.QUESTION_1.ANSWER_2',
                  'MODELS.QUIZ.QUESTION_1.NOTES_1',
              'MODELS.QUIZ.QUESTION_2.QUESTION',
                  'MODELS.QUIZ.QUESTION_2.ANSWER_1', 'MODELS.QUIZ.QUESTION_2.ANSWER_2', 'MODELS.QUIZ.QUESTION_2.ANSWER_3', 'MODELS.QUIZ.QUESTION_2.ANSWER_4',
                  'MODELS.QUIZ.QUESTION_2.NOTES_1', 'MODELS.QUIZ.QUESTION_2.NOTES_2',
              'MODELS.QUIZ.QUESTION_3.QUESTION',
                  'MODELS.QUIZ.QUESTION_3.ANSWER_1', 'MODELS.QUIZ.QUESTION_3.ANSWER_2', 'MODELS.QUIZ.QUESTION_3.ANSWER_3',
                  'MODELS.QUIZ.QUESTION_3.NOTES_1', 'MODELS.QUIZ.QUESTION_3.NOTES_2',
              'MODELS.QUIZ.QUESTION_4.QUESTION',
                  'MODELS.QUIZ.QUESTION_4.ANSWER_1', 'MODELS.QUIZ.QUESTION_4.ANSWER_2', 'MODELS.QUIZ.QUESTION_4.ANSWER_3', 'MODELS.QUIZ.QUESTION_4.ANSWER_4', 'MODELS.QUIZ.QUESTION_4.ANSWER_5', 'MODELS.QUIZ.QUESTION_4.ANSWER_6',
                  'MODELS.QUIZ.QUESTION_4.NOTES_1', 'MODELS.QUIZ.QUESTION_4.NOTES_2',
              'MODELS.QUIZ.QUESTION_5.QUESTION',
                  'MODELS.QUIZ.QUESTION_5.ANSWER_1', 'MODELS.QUIZ.QUESTION_5.ANSWER_2',
                  'MODELS.QUIZ.QUESTION_5.NOTES_1', 'MODELS.QUIZ.QUESTION_5.NOTES_2', 'MODELS.QUIZ.QUESTION_5.NOTES_3', 'MODELS.QUIZ.QUESTION_5.NOTES_4',
              'MODELS.QUIZ.QUESTION_6.QUESTION',
                  'MODELS.QUIZ.QUESTION_6.ANSWER_1', 'MODELS.QUIZ.QUESTION_6.ANSWER_2', 'MODELS.QUIZ.QUESTION_6.ANSWER_3', 'MODELS.QUIZ.QUESTION_6.ANSWER_4', 'MODELS.QUIZ.QUESTION_6.ANSWER_5',
              'MODELS.QUIZ.QUESTION_7.QUESTION',
                  'MODELS.QUIZ.QUESTION_7.ANSWER_1', 'MODELS.QUIZ.QUESTION_7.ANSWER_2', 'MODELS.QUIZ.QUESTION_7.ANSWER_3',
                  'MODELS.QUIZ.QUESTION_7.NOTES_1', 'MODELS.QUIZ.QUESTION_7.NOTES_2', 'MODELS.QUIZ.QUESTION_7.NOTES_3',
              'MODELS.QUIZ.QUESTION_8.QUESTION',
                  'MODELS.QUIZ.QUESTION_8.ANSWER_1', 'MODELS.QUIZ.QUESTION_8.ANSWER_2',
                  'MODELS.QUIZ.QUESTION_8.NOTES_1', 'MODELS.QUIZ.QUESTION_8.NOTES_2', 'MODELS.QUIZ.QUESTION_8.NOTES_3', 'MODELS.QUIZ.QUESTION_8.NOTES_4', 'MODELS.QUIZ.QUESTION_8.NOTES_5',
              'MODELS.QUIZ.QUESTION_9.QUESTION',
                  'MODELS.QUIZ.QUESTION_9.ANSWER_1', 'MODELS.QUIZ.QUESTION_9.ANSWER_2',
                  'MODELS.QUIZ.QUESTION_9.NOTES_1', 'MODELS.QUIZ.QUESTION_9.NOTES_2', 'MODELS.QUIZ.QUESTION_9.NOTES_3', 'MODELS.QUIZ.QUESTION_9.NOTES_4',
              'MODELS.QUIZ.QUESTION_10.QUESTION',
                  'MODELS.QUIZ.QUESTION_10.ANSWER_1', 'MODELS.QUIZ.QUESTION_10.ANSWER_2', 'MODELS.QUIZ.QUESTION_10.ANSWER_3', 'MODELS.QUIZ.QUESTION_10.ANSWER_4',
                  'MODELS.QUIZ.QUESTION_10.NOTES_1', 'MODELS.QUIZ.QUESTION_10.NOTES_2',
              'MODELS.QUIZ.QUESTION_11.QUESTION',
                  'MODELS.QUIZ.QUESTION_11.ANSWER_1', 'MODELS.QUIZ.QUESTION_11.ANSWER_2', 'MODELS.QUIZ.QUESTION_11.ANSWER_3',
                  'MODELS.QUIZ.QUESTION_11.NOTES_1', 'MODELS.QUIZ.QUESTION_11.NOTES_2',
              'MODELS.QUIZ.QUESTION_12.QUESTION',
                  'MODELS.QUIZ.QUESTION_12.ANSWER_1', 'MODELS.QUIZ.QUESTION_12.ANSWER_2', 'MODELS.QUIZ.QUESTION_12.ANSWER_3',
                  'MODELS.QUIZ.QUESTION_12.NOTES_1', 'MODELS.QUIZ.QUESTION_12.NOTES_2', 'MODELS.QUIZ.QUESTION_12.NOTES_3',
              'MODELS.QUIZ.QUESTION_13.QUESTION',
                  'MODELS.QUIZ.QUESTION_13.ANSWER_1', 'MODELS.QUIZ.QUESTION_13.ANSWER_2',
                  'MODELS.QUIZ.QUESTION_13.NOTES_1'
          ]).then(function (translations) {
              allQuestions = [
                  {
                      question : translations['MODELS.QUIZ.QUESTION_1.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_1.ANSWER_1'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_1.ANSWER_2'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_1.NOTES_1']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_2.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_2.ANSWER_1'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_2.ANSWER_2'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_2.ANSWER_3'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_2.ANSWER_4'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_2.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_2.NOTES_2']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_3.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_3.ANSWER_1'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_3.ANSWER_2'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_3.ANSWER_3'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_3.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_3.NOTES_2']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_4.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_4.ANSWER_1'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_4.ANSWER_2'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_4.ANSWER_3'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_4.ANSWER_4'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_4.ANSWER_5'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_4.ANSWER_6'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_4.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_4.NOTES_2']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_5.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_5.ANSWER_1'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_5.ANSWER_2'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_5.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_5.NOTES_2'],
                          translations['MODELS.QUIZ.QUESTION_5.NOTES_3'],
                          translations['MODELS.QUIZ.QUESTION_5.NOTES_4']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_6.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_6.ANSWER_1'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_6.ANSWER_2'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_6.ANSWER_3'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_6.ANSWER_4'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_6.ANSWER_5'], correct : false }
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_7.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_7.ANSWER_1'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_7.ANSWER_2'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_7.ANSWER_3'], correct : true }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_7.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_7.NOTES_2'],
                          translations['MODELS.QUIZ.QUESTION_7.NOTES_3']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_8.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_8.ANSWER_1'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_8.ANSWER_2'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_8.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_8.NOTES_2'],
                          translations['MODELS.QUIZ.QUESTION_8.NOTES_3'],
                          translations['MODELS.QUIZ.QUESTION_8.NOTES_4'],
                          translations['MODELS.QUIZ.QUESTION_8.NOTES_5']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_9.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_9.ANSWER_1'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_9.ANSWER_2'], correct : true }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_9.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_9.NOTES_2'],
                          translations['MODELS.QUIZ.QUESTION_9.NOTES_3'],
                          translations['MODELS.QUIZ.QUESTION_9.NOTES_4']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_10.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_10.ANSWER_1'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_10.ANSWER_2'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_10.ANSWER_3'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_10.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_10.NOTES_2']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_11.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_11.ANSWER_1'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_11.ANSWER_2'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_11.ANSWER_3'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_11.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_11.NOTES_2']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_12.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_12.ANSWER_1'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_12.ANSWER_2'], correct : true },
                          { text : translations['MODELS.QUIZ.QUESTION_12.ANSWER_3'], correct : false }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_12.NOTES_1'],
                          translations['MODELS.QUIZ.QUESTION_12.NOTES_2'],
                          translations['MODELS.QUIZ.QUESTION_12.NOTES_3']
                      ]
                  },
                  {
                      question : translations['MODELS.QUIZ.QUESTION_13.QUESTION'],
                      attempted : false,
                      answers : [
                          { text : translations['MODELS.QUIZ.QUESTION_13.ANSWER_1'], correct : false },
                          { text : translations['MODELS.QUIZ.QUESTION_13.ANSWER_2'], correct : true }
                      ],
                      notes : [
                          translations['MODELS.QUIZ.QUESTION_13.NOTES_1']
                      ]
                  }
              ];
          });
  
  
          function random(numChoices) {
              return Math.floor(Math.random() * (numChoices + 1));
          }
  
          function getQuestion() {
              if (allQuestions.length > 0) {
                  var randomChoiceIdx = random(allQuestions.length - 1);
                  var randomChoice = allQuestions.splice(randomChoiceIdx, 1);
                  var question = randomChoice[0];
                  question.answers.forEach(function (answer) {
                      answer.selected = false;
                  });
                  return question;
              }
          }
  
          function restoreQuestion(question) {
              question.attempted = true;
              allQuestions.push(question);
          }
  
          return {
              getQuestion : getQuestion,
              restoreQuestion : restoreQuestion
          };
      }
  })();
  
      (function () {
  
          angular
              .module('app')
              .controller('ScratchController', ScratchController);
  
          ScratchController.$inject = [
              'authService',
              'projectsService', 'scratchkeysService',
              '$stateParams',
              '$scope', '$timeout'
          ];
  
          function ScratchController(authService, projectsService, scratchkeysService, $stateParams, $scope, $timeout) {
  
              var vm = this;
              vm.authService = authService;
  
              var alertId = 1;
              vm.errors = [];
              vm.warnings = [];
              vm.dismissAlert = function (type, errIdx) {
                  vm[type].splice(errIdx, 1);
              };
              function displayAlert(type, errObj) {
                  if (!errObj) {
                      errObj = {};
                  }
                  vm[type].push({ alertid : alertId++, message : errObj.message || errObj.error || 'Unknown error', status : errObj.status });
              }
  
  
              $scope.projectId = $stateParams.projectId;
              $scope.userId = $stateParams.userId;
  
              $scope.projecturls = {
                  train : '/#!/mlproject/' + $stateParams.userId + '/' + $stateParams.projectId + '/training',
                  learnandtest : '/#!/mlproject/' + $stateParams.userId + '/' + $stateParams.projectId + '/models'
              };
  
              $scope.scratchblocks = {
                  label : '',
                  confidence : '',
                  sample : ''
              };
  
  
              authService.getProfileDeferred()
                  .then(function (profile) {
                      vm.profile = profile;
  
                      return projectsService.getProject($scope.projectId, $scope.userId, profile.tenant);
                  })
                  .then(function (project) {
                      $scope.project = project;
  
                      $scope.projecturls.train = '/#!/mlproject/' + $scope.project.userid + '/' + $scope.project.id + '/training';
                      $scope.projecturls.learnandtest = '/#!/mlproject/' + $scope.project.userid + '/' + $scope.project.id + '/models';
  
                      if (project.type === 'numbers') {
                          return projectsService.getFields($scope.projectId, $scope.userId, vm.profile.tenant);
                      }
                      else {
                          return;
                      }
                  })
                  .then(function (fields) {
                      $scope.scratchblocks.label = 'recognise ' + $scope.project.type + ' ';
                      if ($scope.project.type === 'text') {
                          $scope.scratchblocks.label += '[text]';
                      }
                      else if ($scope.project.type === 'images') {
                          $scope.scratchblocks.label += '[costume image]';
                      }
                      else if ($scope.project.type === 'numbers') {
                          var idx = 1;
                          for (var fldIndex in fields) {
                              var field = fields[fldIndex].name;
                              $scope.scratchblocks.label += field + ' (' + (idx++) + ') ';
                          }
                      }
                      $scope.scratchblocks.confidence = $scope.scratchblocks.label + ' \\(confidence) :: custom reporter';
                      $scope.scratchblocks.label += ' \\(label) :: custom reporter';
  
                      if ($scope.project.type === 'text') {
                          $scope.scratchblocks.sample =
                              'ask [enter some text here] and wait \n' +
                              'if &lt;{recognise text (answer) \\(label) :: custom reporter } = (' + $scope.project.labels[0] + ' :: custom reporter)&gt; then \n' +
                              'say [I think that was ' + $scope.project.labels[0] + ']';
                      }
                      else if ($scope.project.type === 'images') {
                          $scope.scratchblocks.sample =
                              'if &lt;{recognise images (costume image :: looks) :: custom reporter } = (' + $scope.project.labels[0] + ' :: custom reporter)&gt; then \n' +
                              'say [I think that is a picture of ' + $scope.project.labels[0] + ']';
                      }
                      else if ($scope.project.type === 'numbers') {
                          $scope.scratchblocks.sample =
                              'if &lt;{' + $scope.scratchblocks.label + '} = (' + $scope.project.labels[0] + ' :: custom reporter)&gt; then \n' +
                              'say [I think that was ' + $scope.project.labels[0] + ']';
                      }
  
                      $timeout(function () {
                          scratchblocks.renderMatching('.scratchblocks');
                      }, 50);
  
                      return scratchkeysService.getScratchKeys($scope.project.id, $scope.userId, vm.profile.tenant);
                  })
                  .then(function (resp) {
                      var scratchkey = resp[0];
  
                      scratchkey.extensionurl = window.location.origin +
                                              '/api/scratch/' +
                                              scratchkey.id +
                                              '/extension.js'
  
                      scratchkey.url = '/scratchx?url=' +
                                      scratchkey.extensionurl +
                                      '#scratch';
  
                      $scope.scratchkey = scratchkey;
                  })
                  .catch(function (err) {
                      displayAlert('errors', err.data);
                  });
          }
  
      }());
  
      (function () {
  
          angular
              .module('app')
              .controller('Scratch3Controller', Scratch3Controller);
  
          Scratch3Controller.$inject = [
              'authService',
              'projectsService', 'scratchkeysService',
              '$stateParams',
              '$scope'
          ];
  
          function Scratch3Controller(authService, projectsService, scratchkeysService, $stateParams, $scope) {
  
              var vm = this;
              vm.authService = authService;
  
              $scope.projectId = $stateParams.projectId;
              $scope.userId = $stateParams.userId;
  
              $scope.projecturls = {
                  train : '/#!/mlproject/' + $stateParams.userId + '/' + $stateParams.projectId + '/training',
                  learnandtest : '/#!/mlproject/' + $stateParams.userId + '/' + $stateParams.projectId + '/models'
              };
  
              authService.getProfileDeferred()
                  .then(function (profile) {
                      vm.profile = profile;
  
                      return projectsService.getProject($scope.projectId, $scope.userId, profile.tenant);
                  })
                  .then(function (project) {
                      $scope.project = project;
  
                      $scope.projecturls.train = '/#!/mlproject/' + $scope.project.userid + '/' + $scope.project.id + '/training';
                      $scope.projecturls.learnandtest = '/#!/mlproject/' + $scope.project.userid + '/' + $scope.project.id + '/models';
  
                      return scratchkeysService.getScratchKeys(project.id, $scope.userId, vm.profile.tenant);
                  })
                  .then(function (resp) {
                      var scratchkey = resp[0];
  
                      scratchkey.extensionurl = window.location.origin +
                                                '/api/scratch/' +
                                                scratchkey.id +
                                                '/extension3.js'
  
                      if ($scope.project.type === 'sounds') {
                          scratchkey.model = 'placeholder';
                      }
  
                      $scope.scratchkey = scratchkey;
                  })
                  .catch(function (err) {
                      $scope.failure = {
                          message : err.data.error,
                          status : err.status
                      };
                  });
          }
      }());
  
  (function () {
  
      angular
          .module('app')
          .service('scratchkeysService', scratchkeysService);
  
      scratchkeysService.$inject = [
          '$q', '$http'
      ];
  
      function scratchkeysService($q, $http) {
  
          function getScratchKeys(projectid, userid, tenant) {
              return $http.get('/api/classes/' + tenant + '/students/' + userid + '/projects/' + projectid + '/scratchkeys')
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
  
          return {
              getScratchKeys : getScratchKeys
          };
      }
  })();
  
  (function () {
  
          angular
              .module('app')
              .controller('SignupController', SignupController);
  
          SignupController.$inject = [
              'authService',
              'usersService',
              '$document', '$timeout'
          ];
  
          function SignupController(authService, usersService, $document, $timeout) {
              var vm = this;
              vm.authService = authService;
  
              vm.focused = '';
  
              var alertId = 1;
              vm.errors = [];
              vm.infos = [];
              vm.dismissAlert = function (type, errIdx) {
                  vm[type].splice(errIdx, 1);
              };
              function displayAlert(type, status, errObj) {
                  if (!errObj) {
                      errObj = {};
                  }
                  var newId = alertId++;
                  vm[type].push({
                      alertid : newId,
                      message : errObj.message || errObj.error || 'Unknown error',
                      status : status
                  });
                  scrollToNewItem(type + newId);
              }
              function scrollToNewItem(itemId) {
                  $timeout(function () {
                      var newItem = document.getElementById(itemId);
                      $document.duScrollToElementAnimated(angular.element(newItem));
                  }, 0);
              }
  
              vm.creating = false;
              vm.complete = false;
  
              vm.confirm = function (newClassDetails) {
                  vm.creating = true;
  
                  usersService.createTeacher(newClassDetails.username, newClassDetails.email, newClassDetails.intendeduse)
                      .then(function (resp) {
                          console.log(resp);
  
                          var newId = alertId++;
                          vm.infos.push({
                              alertid : newId,
                              password : resp.password
                          });
                          scrollToNewItem('infos' + newId);
  
                          vm.complete = true;
                      })
                      .catch(function (err) {
                          vm.creating = false;
                          displayAlert('errors', 500, err.data);
                      });
              };
          }
  
      }());
  
  (function () {
  
      angular
          .module('app')
          .service('sitealertsService', sitealertsService);
  
      sitealertsService.$inject = [
          '$http', '$rootScope', '$interval',
          'authService'
      ];
  
      function sitealertsService($http, $rootScope, $interval, authService) {
  
          function logError(err) {
              console.log(err);
              delete $rootScope.siteAlert;
          }
  
          function init() {
              $rootScope.clearSiteAlert = function () {
                  delete $rootScope.siteAlert;
              };
  
              fetchSiteAlert({}, 'init');
  
              $rootScope.$on('authStateChange', fetchSiteAlert);
              $interval(fetchSiteAlert, 300000, 0, true, 'timer');
          }
  
          function getAlerts(endpoint, tenant, userid) {
              var url = '/api/sitealerts/public';
              if (endpoint === 'student') {
                  url = '/api/sitealerts/alerts/' + tenant + '/students/' + userid;
              }
              else if (endpoint === 'supervisor') {
                  url = '/api/sitealerts/alerts/' + tenant + '/supervisors/' + userid;
              }
              return $http.get(url)
                  .then(function (resp) {
                      var msgs = resp.data;
                      if (msgs && msgs.length > 0 &&
                          msgs[0].message && msgs[0].severity)
                      {
                          $rootScope.siteAlert = msgs[0];
                      }
                      else {
                          delete $rootScope.siteAlert;
                      }
                  })
                  .catch(logError);
          }
  
          function fetchSiteAlert(evtObj, evt) {
              // console.log('fetchSiteAlert', evt);
              if ($rootScope.isAuthenticated) {
                  return authService.getProfileDeferred()
                      .then(function (profile) {
                          if (profile && profile.role) {
                              return getAlerts(profile.role, profile.tenant, profile.user_id);
                          }
                          else {
                              return logError('Invalid profile');
                          }
                      })
                      .catch(logError);
              }
              else {
                  return getAlerts('public');
              }
          }
  
          function createAlert (details) {
              return $http.post('/api/sitealerts', details);
          }
  
          function refreshServer () {
              return $http.put('/api/sitealerts/actions/refresh');
          }
  
          return {
              init : init,
              createAlert : createAlert,
              refreshServer : refreshServer
          };
      }
  })();
  
  (function () {
  
      angular
          .module('app')
          .service('soundTrainingService', soundTrainingService);
  
      soundTrainingService.$inject = [
          'trainingService', 'utilService', '$q'
      ];
  
      function soundTrainingService(trainingService, utilService, $q) {
  
          var transferRecognizer;
          var transferModelInfo;
  
          var modelStatus;
  
          function isUserMediaSupported() {
              return navigator &&
                     navigator.mediaDevices &&
                     navigator.mediaDevices.getUserMedia;
          }
  
  
          // easiest way to see if we're allowed to access the microphone
          //  is to try and access the microphone   ¯\_(ツ)_/¯
          function permissionsCheck() {
              return navigator.mediaDevices.getUserMedia({ audio : true, video : false })
                  .then(function (stream) {
                      stream.getTracks().forEach(function (track) {
                          track.stop();
                      });
                  })
                  .catch(function (err) {
                      if (err.name === 'NotAllowedError' || err.name === 'SecurityError') {
                          throw { status : 400, data : {
                              message : 'Sorry! Machine Learning for Kids was not allowed to use your microphone'
                          }};
                      }
                      else if (err.name === 'NotFoundError' || err.name === 'TypeError') {
                          throw { status : 400, data : {
                              message : 'Sorry! Machine Learning for Kids could not find a microphone to use'
                          }};
                      }
                      else if (err.name === 'NotReadableError') {
                          throw { status : 400, data : {
                              message : 'Sorry! There was a problem with your microphone'
                          }};
                      }
                      else {
                          // record the error
                          console.log(err);
                          if (err && Sentry && Sentry.captureException) {
                              Sentry.captureException(err);
                          }
  
                          throw { status : 500, data : err };
                      }
                  });
          }
  
          function loadTensorFlow() {
              if (!isUserMediaSupported()) {
                  if (utilService.isInternetExplorer()) {
                      throw ({
                          status : 400,
                          data : { message : 'Sorry! Internet Explorer cannot be used for sounds projects' }
                      });
                  }
                  else {
                      throw ({
                          status : 400,
                          data : { message : 'Sorry! Machine Learning for Kids could not find a microphone to use' }
                      });
                  }
              }
  
              return permissionsCheck()
                  .then(function () {
                      return utilService.loadScript('https://unpkg.com/@tensorflow/tfjs@1.3.1/dist/tf.js');
                  })
                  .then(function () {
                      return utilService.loadScript('https://unpkg.com/@tensorflow-models/speech-commands@0.4.0');
                  })
                  .then(function () {
                      if (tf && tf.enableProdMode) {
                          tf.enableProdMode();
                      }
                  });
          }
  
          function initSoundSupport(projectid) {
              var baseRecognizer;
              return loadTensorFlow()
                  .then(function () {
                      baseRecognizer = speechCommands.create('BROWSER_FFT');
                      return baseRecognizer.ensureModelLoaded();
                  })
                  .then(function () {
                      transferRecognizer = baseRecognizer.createTransfer(projectid);
  
                      var modelInfo = transferRecognizer.modelInputShape();
                      transferModelInfo = {
                          numFrames : modelInfo[1],
                          fftSize : modelInfo[2]
                      };
                  });
          }
  
          function collectExample(label) {
              return transferRecognizer.collectExample(label);
          }
  
          function getModelInfo() {
              return transferModelInfo;
          }
  
          function getModels() {
              return $q(function (resolve) {
                  if (modelStatus) {
                      modelStatus.lastPollTime = new Date();
                      resolve([ modelStatus ]);
                  }
                  else {
                      resolve([]);
                  }
              });
          }
  
          function getTrainingData(projectid, userid, tenantid) {
              return trainingService.getTraining(projectid, userid, tenantid)
                  .then(function (traininginfo) {
                      return $q.all(traininginfo.map(trainingService.getSoundData));
                  });
          }
  
          function newModel(projectid, userid, tenantid) {
              modelStatus = {
                  classifierid : projectid,
                  status : 'Training',
                  progress : 0,
                  updated : new Date()
              };
  
              return getTrainingData(projectid, userid, tenantid)
                  .then(function (trainingdata) {
                      // reset
                      transferRecognizer.dataset.clear();
                      transferRecognizer.dataset.label2Ids = {};
                      transferRecognizer.words = null;
  
                      // add training data
                      for (var i = 0; i < trainingdata.length; i++) {
                          var trainingdataitem = trainingdata[i];
  
                          transferRecognizer.dataset.addExample({
                              label : trainingdataitem.label,
                              spectrogram : {
                                  frameSize : transferModelInfo.fftSize,
                                  data : new Float32Array(trainingdataitem.audiodata)
                              }
                          });
                      }
  
                      // rebuild vocab
                      transferRecognizer.collateTransferWords();
  
                      return tf.nextFrame();
                  })
                  .then(function () {
                      transferRecognizer.train({
                          epochs : 100,
                          callback: {
                              onEpochEnd: function (epoch) {
                                  modelStatus.progress = epoch;
                              }
                          }
                      }).then(function() {
                          modelStatus.status = 'Available';
                          modelStatus.progress = 100;
                      });
  
                      return modelStatus;
                  })
                  .catch(function (err) {
                      console.log(err);
  
                      modelStatus.status = 'Failed';
                      modelStatus.updated = new Date();
  
                      return modelStatus;
                  });
          }
  
  
          function startTest(callback) {
              return transferRecognizer.listen(function (result) {
                  var matches = [];
  
                  var labels = transferRecognizer.wordLabels();
                  if (!labels) {
                      console.log('Labels unavailable');
                      return callback(matches);
                  }
  
                  if (labels.length !== result.scores.length) {
                      console.log('Unexpected number of results',
                                  labels.length,
                                  result.scores.length);
                  }
  
                  for (var i = 0; i < Math.min(labels.length, result.scores.length); i++) {
                      matches.push({
                          class_name : labels[i],
                          confidence : result.scores[i] * 100
                      });
                  }
  
                  matches.sort(function (a, b) {
                      return b.confidence - a.confidence;
                  });
  
                  callback(matches);
              }, {
                  probabilityThreshold : 0.7
              });
          }
  
          function stopTest() {
              return transferRecognizer.stopListening();
          }
  
  
          return {
              initSoundSupport : initSoundSupport,
              getModelInfo : getModelInfo,
              collectExample : collectExample,
              newModel : newModel,
              getModels : getModels,
              startTest : startTest,
              stopTest : stopTest
          };
      }
  })();
  
  (function () {
  
      angular
          .module('app')
          .controller('TeacherApiKeysController', TeacherApiKeysController);
  
      TeacherApiKeysController.$inject = [
          'authService',
          'usersService',
          '$stateParams', '$mdDialog', '$document', '$timeout'
      ];
  
      function TeacherApiKeysController(authService, usersService, $stateParams, $mdDialog, $document, $timeout) {
  
          var vm = this;
          vm.authService = authService;
  
          vm.CONSTANTS = {
              UNKNOWN : -1,
              UNLIMITED : -2
          };
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
              var newId = alertId++;
              vm[type].push({
                  alertid : newId,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
              return newId;
          }
  
  
          function computeLimit(type) {
              var creds = vm.credentials[type];
  
              var mlmodels = 0;
              for (var i = 0; i < creds.length; i++) {
                  var cred = creds[i];
                  if (cred.credstype === 'conv_lite') {
                      mlmodels += 5;
                  }
                  else if (cred.credstype === 'conv_standard') {
                      mlmodels += 20;
                  }
                  else if (cred.credstype === 'visrec_lite') {
                      mlmodels += 2;
                  }
                  else if (cred.credstype === 'visrec_standard') {
                      mlmodels = vm.CONSTANTS.UNLIMITED;
                      break;
                  }
                  else {
                      mlmodels = vm.CONSTANTS.UNKNOWN;
                      break;
                  }
              }
              vm.credentials.totals[type] = mlmodels;
          }
  
          function getCredentials(profile, type) {
              usersService.getCredentials(profile, type)
                  .then(function (creds) {
                      vm.credentials[type] = creds;
                      vm.credentials.loading[type] = false;
  
                      computeLimit(type);
                  })
                  .catch(function (err) {
                      vm.credentials.failed[type] = true;
                      vm.credentials.loading[type] = false;
                      displayAlert('errors', err.status, err.data);
                  });
          }
  
  
          function getAllCredentials(profile) {
              vm.credentials = {
                  loading : {
                      visrec : true,
                      conv : true
                  },
                  failed : {
                      visrec : false,
                      conv : false
                  },
                  totals : {
                      visrec : vm.CONSTANTS.UNKNOWN,
                      conv : vm.CONSTANTS.UNKNOWN
                  }
              };
              getCredentials(profile, 'visrec');
              getCredentials(profile, 'conv');
          }
  
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  if (profile.role === 'supervisor') {
  
                      usersService.getClassPolicy(profile)
                          .then(function (policy) {
                              vm.policy = policy;
  
                              if (vm.policy.isManaged === false) {
                                  getAllCredentials(profile);
                              }
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
                          });
                  }
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data);
              });
  
  
          vm.deleteCredentials = function (ev, creds, type) {
              var confirm = $mdDialog.confirm()
                  .title('Are you sure?')
                  .textContent('Do you want to remove these credentials from machinelearningforkids.co.uk?')
                  .ariaLabel('Confirm')
                  .targetEvent(ev)
                  .ok('Yes')
                  .cancel('No');
  
              $mdDialog.show(confirm).then(
                  function() {
                      usersService.deleteCredentials(vm.profile, creds)
                          .then(function () {
                              vm.credentials[type] = vm.credentials[type].filter(function (itm) {
                                  return itm.id !== creds.id;
                              });
                              computeLimit(type);
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
                          });
  
                  },
                  function() {
                      // cancelled. do nothing
                  });
          };
  
  
          vm.addCredentials = function (ev, type) {
              $mdDialog.show({
                  controller : function ($scope, $mdDialog) {
                      $scope.type = 'apikey';
                      $scope.credstype = '';
  
                      $scope.hide = function () {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function () {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function (resp) {
                          $mdDialog.hide(resp);
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/teacher_apikeys/newcreds' + type + '.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function(credentialsToAdd) {
                      credentialsToAdd.servicetype = type;
                      credentialsToAdd.isPlaceholder = true;
  
                      var placeholder = Date.now();
                      credentialsToAdd.uniq = placeholder;
  
                      vm.credentials[type].push(credentialsToAdd);
  
                      usersService.addCredentials(credentialsToAdd, vm.profile.tenant)
                          .then(function (newcreds) {
                              vm.credentials[type] = vm.credentials[type].filter(function (c) {
                                  return c.uniq !== placeholder;
                              });
  
                              vm.credentials[type].push(newcreds);
                              computeLimit(type);
                          })
                          .catch(function (err) {
                              var errId = displayAlert('errors', err.status, err.data);
                              scrollToNewItem('errors' + errId);
  
                              vm.credentials[type] = vm.credentials[type].filter(function (c) {
                                  return c.uniq !== placeholder;
                              });
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
          vm.modifyCredentials = function (ev, creds, type) {
              $mdDialog.show({
                  controller : function ($scope, $mdDialog) {
                      $scope.credstype = creds.credstype;
                      $scope.currentcredstype = creds.credstype;
  
                      $scope.hide = function () {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function () {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function (resp) {
                          $mdDialog.hide(resp);
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/teacher_apikeys/modifycreds' + type + '.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function(modifyRequest) {
                      usersService.modifyCredentials(creds, type, modifyRequest.credstype, vm.profile.tenant)
                          .then(function () {
                              creds.credstype = modifyRequest.credstype;
                              computeLimit(type);
                          })
                          .catch(function (err) {
                              var errId = displayAlert('errors', err.status, err.data);
                              scrollToNewItem('errors' + errId);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
          vm.explainLimit = function () {
              var alert = $mdDialog.alert()
                              .title('Limit on the number of machine learning models')
                              .textContent('When a student in your class clicks on the "Train machine learning model" ' +
                                           'button, the model that they create will count towards this limit. If this limit ' +
                                           'is exceeded, they will see an error saying that the class has already created ' +
                                           'their maximum allowed number of models. See the "Help" page for suggestions for ' +
                                           'how to avoid this')
                              .ok('OK');
              $mdDialog.show(alert)
          };
  
  
          function scrollToNewItem(itemId) {
              $timeout(function () {
                  var newItem = document.getElementById(itemId);
                  $document.duScrollToElementAnimated(angular.element(newItem));
              }, 0);
          }
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('TeacherRestrictionsController', TeacherRestrictionsController);
  
      TeacherRestrictionsController.$inject = [
          'authService',
          'usersService',
          '$stateParams', '$mdDialog'
      ];
  
      function TeacherRestrictionsController(authService, usersService, $stateParams, $mdDialog) {
  
          var vm = this;
          vm.authService = authService;
  
          vm.saving = false;
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
              var newId = alertId++;
              vm[type].push({
                  alertid : newId,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
              return newId;
          }
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  if (profile.role === 'supervisor') {
  
                      usersService.getClassPolicy(profile)
                          .then(function (policy) {
                              vm.policy = policy;
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
                          });
                  }
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data);
              });
  
          vm.modifyExpiry = function (ev) {
              var initialtextexpiry = vm.policy.textClassifierExpiry;
              var initialimageexpiry = vm.policy.imageClassifierExpiry;
  
              $mdDialog.show({
                  controller : function ($scope, $mdDialog) {
                      $scope.initialtextexpiry = initialtextexpiry;
                      $scope.initialimageexpiry = initialimageexpiry;
  
                      $scope.textexpiry = vm.policy.textClassifierExpiry;
                      $scope.imageexpiry = vm.policy.imageClassifierExpiry;
  
                      $scope.hoursfilter = function (hours) {
                          if (hours === 1) {
                              return '1 hour';
                          }
                          else if (hours < 24) {
                              return hours + ' hours';
                          }
                          else if (hours === 24) {
                              return '1 day';
                          }
                          else if (hours < 168) {
                              return Math.floor(hours / 24) + ' days, ' + (hours % 24) + ' hours';
                          }
                          else if (hours === 168) {
                              return '1 week';
                          }
                          else {
                              return '1 week, ' +
                                     Math.floor((hours - 168) / 24) + ' days, ' + (hours % 24) + ' hours';
                          }
                      };
  
                      $scope.hide = function () {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function () {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function (resp) {
                          $mdDialog.hide(resp);
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/teacher_restrictions/modifyexpiry.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (expiries) {
                      vm.saving = true;
                      vm.policy.textClassifierExpiry = '...';
                      vm.policy.imageClassifierExpiry = '...';
  
                      usersService.modifyClassPolicy(vm.profile, expiries.textexpiry, expiries.imageexpiry)
                          .then(function (tenantinfo) {
                              vm.saving = false;
                              vm.policy.textClassifierExpiry = tenantinfo.textClassifierExpiry;
                              vm.policy.imageClassifierExpiry = tenantinfo.imageClassifierExpiry;
                          })
                          .catch(function (err) {
                              vm.saving = false;
                              displayAlert('errors', err.status, err.data);
                              vm.policy.textClassifierExpiry = initialtextexpiry;
                              vm.policy.imageClassifierExpiry = initialimageexpiry;
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              )
          };
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('TeacherStudentsController', TeacherStudentsController);
  
      TeacherStudentsController.$inject = [
          'authService',
          'usersService',
          '$stateParams', '$mdDialog', '$document', '$timeout'
      ];
  
      function TeacherStudentsController(authService, usersService, $stateParams, $mdDialog, $document, $timeout) {
  
          var vm = this;
          vm.authService = authService;
  
          vm.allStudentPasswordsReset = false;
  
          var placeholderId = 1;
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
              var newId = alertId++;
              vm[type].push({
                  alertid : newId,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
              return newId;
          }
  
  
          function refreshStudentsList(profile) {
              usersService.getStudentList(profile)
                  .then(function (students) {
                      vm.students = students;
                  })
                  .catch(function (err) {
                      displayAlert('errors', err.status, err.data);
                  });
          }
  
  
          function displayPassword(ev, student, showWarning) {
              $mdDialog.show(
                  $mdDialog.alert()
                      .clickOutsideToClose(true)
                      .title(student.username)
                      .htmlContent(
                          '<div>Password: <span class="passworddisplaydialog">' + student.password + '</span></div>' +
                          (showWarning ? '<div><strong>Note:</strong> This may take a few minutes to take effect. Please be patient.' : ''))
                      .ariaLabel('Confirm student password')
                      .ok('OK')
                      .targetEvent(ev)
                  );
          }
  
  
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  if (profile.role === 'supervisor') {
                      refreshStudentsList(profile);
  
                      usersService.getClassPolicy(profile)
                          .then(function (policy) {
                              vm.policy = policy;
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
                          });
                  }
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data);
              });
  
  
          vm.deleteUser = function (ev, student) {
              var confirm = $mdDialog.confirm()
                  .title('Are you sure?')
                  .textContent('Do you want to delete ' + student.username + ' and all of their work? (This cannot be undone)')
                  .ariaLabel('Confirm')
                  .targetEvent(ev)
                  .ok('Yes')
                  .cancel('No');
  
              $mdDialog.show(confirm).then(
                  function() {
                      student.isPlaceholder = true;
  
                      usersService.deleteStudent(student, vm.profile.tenant)
                          .then(function () {
                              vm.students = vm.students.filter(function (itm) {
                                  return itm.username !== student.username;
                              });
                          })
                          .catch(function (err) {
                              student.isPlaceholder = false;
                              displayAlert('errors', err.status, err.data);
                          });
  
                  },
                  function() {
                      // cancelled. do nothing
                  });
          };
  
  
  
          vm.createMultipleUsers = function (ev) {
              $mdDialog.show({
                  controller : function ($scope, $mdDialog) {
                      $scope.remaining = vm.policy.maxUsers - vm.students.length - 1;
                      $scope.userslimit = vm.policy.maxUsers;
  
                      $scope.hide = function () {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function () {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function (prefix, number, password) {
                          $mdDialog.hide({
                              prefix : prefix,
                              number : number,
                              password : password
                          });
                      };
                      $scope.refreshPassword = function () {
                          $scope.password = '...';
                          usersService.getGeneratedPassword(vm.profile.tenant)
                              .then(function (resp) {
                                  $scope.password = resp.password;
                              })
                              .catch(function (err) {
                                  console.log(err);
                              });
                      };
  
                      $scope.refreshPassword();
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/teacher_students/newstudents.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function(dialogResp) {
                      for (var i = 1; i <= dialogResp.number; i++) {
                          var newUserObj = {
                              id : placeholderId++,
                              username : dialogResp.prefix + i,
                              isPlaceholder : true
                          };
                          vm.students.push(newUserObj);
                      }
  
                      usersService.createStudents(vm.profile.tenant, dialogResp.prefix, dialogResp.number, dialogResp.password)
                          .then(function (apiResp) {
                              vm.students = vm.students.filter(function (student) {
                                  return !student.isPlaceholder;
                              });
  
                              if (apiResp && apiResp.successes) {
                                  for (var i = 0; i < apiResp.successes.length; i++) {
                                      vm.students.push(apiResp.successes[i]);
                                  }
                              }
  
                              displayCreateFailures(ev, apiResp, dialogResp.password);
                          })
                          .catch(function (err) {
                              vm.students = vm.students.filter(function (student) {
                                  return !student.isPlaceholder;
                              });
  
                              displayAlert('errors', err.status, err.data);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
  
          vm.createUser = function (ev) {
              $mdDialog.show({
                  controller : function ($scope, $mdDialog) {
                      $scope.hide = function () {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function () {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function (resp) {
                          $mdDialog.hide(resp);
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/teacher_students/newstudent.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function(username) {
                      var newUserObj = {
                          id : placeholderId++,
                          username : username,
                          isPlaceholder : true
                      };
                      vm.students.push(newUserObj);
  
                      usersService.createStudent(username, vm.profile.tenant)
                          .then(function (newUser) {
                              newUserObj.id = newUser.id;
                              newUserObj.isPlaceholder = false;
  
                              displayPassword(ev, newUser);
                          })
                          .catch(function (err) {
                              var errId = displayAlert('errors', err.status, err.data);
                              scrollToNewItem('errors' + errId);
  
                              vm.students = vm.students.filter(function (student) {
                                  return student.id !== newUserObj.id;
                              });
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
          vm.resetUserPassword = function (ev, student) {
              student.isPlaceholder = true;
  
              usersService.resetStudentPassword(student, vm.profile.tenant)
                  .then(function (updatedUser) {
                      student.isPlaceholder = false;
                      displayPassword(ev, updatedUser);
                  })
                  .catch(function (err) {
                      student.isPlaceholder = false;
                      displayAlert('errors', err.status, err.data);
                  });
          };
  
          vm.resetUsersPassword = function (ev) {
              var confirm = $mdDialog.confirm()
                  .title('Are you sure?')
                  .textContent('Do you want to reset the passwords for all your students (so they all have the same password)?')
                  .ariaLabel('Confirm')
                  .targetEvent(ev)
                  .ok('Yes')
                  .cancel('No');
  
              $mdDialog.show(confirm).then(
                  function() {
                      vm.allStudentPasswordsReset = true;
  
                      vm.students.forEach(function (student) {
                          student.isPlaceholder = true;
                      });
  
                      usersService.resetStudentsPassword(vm.students, vm.profile.tenant)
                          .then(function (resp) {
                              vm.students.forEach(function (student) {
                                  student.isPlaceholder = false;
                              });
                              displayPassword(ev, {
                                  username : 'All students',
                                  password : resp.password
                              }, vm.students.length > 40);
                          })
                          .catch(function (err) {
                              vm.students.forEach(function (student) {
                                  student.isPlaceholder = false;
                              });
                              vm.allStudentPasswordsReset = false;
                              displayAlert('errors', err.status, err.data);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  });
          };
  
  
          function displayCreateFailures (ev, resp, password) {
              if (resp && resp.successes && resp.failures && resp.duplicates) {
  
                  if (resp.failures.length > 0 || resp.duplicates.length > 0)
                  {
                      var title = resp.failures.length > 0 ?
                                      'Something went wrong!' :
                                      'Usernames already in use';
  
                      var message = '';
  
                      if (resp.failures.length > 0) {
                          message = '<div>Sorry. An unexpected error happened when trying to create: <br/>' +
                                      '<code>' + resp.failures.join(', ') + '</code>' +
                                  '</div>';
                      }
                      if (resp.duplicates.length > 0) {
                          message = '<div>The following student accounts could not be created because there are ' +
                                      'already users with these usernames: <br/>' +
                                      '<code>' + resp.duplicates.join(', ') + '</code>' +
                                  '</div>';
                      }
  
                      displayErrorMessage(ev, title,
                                          '<div style="padding: 1em">' + message + '</div>');
                  }
                  else if (resp.successes.length > 0) {
                      displayPassword(ev, {
                          username : 'New students created:',
                          password : password
                      }, false);
                  }
              }
              else {
                  displayAlert('errors', 500, { error : 'Unexpected response' });
              }
          }
  
          function displayErrorMessage(ev, title, contents) {
              $mdDialog.show(
                  $mdDialog.alert()
                      .clickOutsideToClose(true)
                      .title(title)
                      .htmlContent(contents)
                      .ok('OK')
                      .targetEvent(ev)
                  );
          }
  
          function scrollToNewItem(itemId) {
              $timeout(function () {
                  var newItem = document.getElementById(itemId);
                  $document.duScrollToElementAnimated(angular.element(newItem));
              }, 0);
          }
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('TeacherProjectsController', TeacherProjectsController);
  
      TeacherProjectsController.$inject = [
          'authService',
          'usersService',
          'projectsService', 'trainingService',
          '$stateParams', '$mdDialog', '$document', '$timeout', '$scope'
      ];
  
      function TeacherProjectsController(authService, usersService, projectsService, trainingService, $stateParams, $mdDialog, $document, $timeout, $scope) {
  
          var vm = this;
          vm.authService = authService;
  
          var placeholderId = 1;
  
          $scope.submittingDeleteRequest = false;
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
              var newId = alertId++;
              vm[type].push({
                  alertid : newId,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
              return newId;
          }
  
  
          vm.orderBy = 'name';
  
  
  
          function refreshProjectsList(profile) {
              projectsService.getClassProjects(profile)
                  .then(function (projects) {
                      vm.projects = projects;
  
                      for (var i = 0; i < vm.projects.length; i++) {
                          var project = vm.projects[i];
  
                          if (project.labels.length > 0) {
                              var summary = '';
                              switch (project.labels.length) {
                                  case 1:
                                      summary = project.labels[0];
                                      break;
                                  case 2:
                                      summary = project.labels[0] + ' or ' + project.labels[1];
                                      break;
                                  case 3:
                                      summary = project.labels[0] + ', ' +
                                                project.labels[1] + ' or ' +
                                                project.labels[2];
                                      break;
                                  default:
                                      summary = project.labels[0] + ', ' +
                                                project.labels[1] + ' or ' +
                                                (project.labels.length - 2) + ' other classes';
                                      break;
                              }
                              project.labelsSummary = summary;
                          }
                      }
                  })
                  .catch(function (err) {
                      displayAlert('errors', err.status, err.data);
                  });
          }
  
  
  
          function refreshClassifiersList(profile) {
              trainingService.getUnmanagedClassifiers(profile.tenant)
                  .then(function (classifiers) {
                      vm.classifiers = classifiers;
                  })
                  .catch(function (err) {
                      if (err && err.status && err.status === 403) {
                          // probably a managed tenant - so they're not allowed
                          //  to review unmanaged classifiers (this is sorted
                          //  for them)
                      }
                      else {
                          console.log(err);
                      }
                  });
          }
  
  
  
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  if (profile.role === 'supervisor') {
                      refreshProjectsList(profile);
                      refreshClassifiersList(profile);
                  }
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data);
              });
  
  
          vm.deleteModel = function (ev, project) {
              var confirm = $mdDialog.confirm()
                  .title('Are you sure?')
                  .textContent('Do you want to delete ' +
                               (project.owner ? project.owner.username + '\'s ' : '') +
                               'machine learning model from project ' + project.name + '?')
                  .ariaLabel('Confirm')
                  .targetEvent(ev)
                  .ok('Yes')
                  .cancel('No');
  
              $mdDialog.show(confirm).then(
                  function() {
                      project.hasModel = false;
                      trainingService.deleteModel(project.id, project.userid, project.classid, project.classifierId)
                          .then(function () {
                              console.log('deletion successful');
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
          vm.deleteClassifier = function (ev, classifier) {
              $scope.submittingDeleteRequest = true;
  
              var confirm = $mdDialog.confirm()
                  .title('Are you sure?')
                  .textContent('Do you want to delete ' + classifier.name + '?')
                  .ariaLabel('Confirm')
                  .targetEvent(ev)
                  .ok('Yes')
                  .cancel('No');
  
              $mdDialog.show(confirm).then(
                  function() {
                      trainingService.deleteBluemixClassifier(vm.profile.tenant, classifier.id, classifier.credentials.id, classifier.type)
                          .then(function () {
                              $scope.submittingDeleteRequest = false;
                              vm.classifiers[classifier.type] = vm.classifiers[classifier.type].filter(function (c) {
                                  return c.id !== classifier.id;
                              });
                          })
                          .catch(function (err) {
                              $scope.submittingDeleteRequest = false;
                              displayAlert('errors', err.status, err.data);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
  
          };
  
  
          function scrollToNewItem(itemId) {
              $timeout(function () {
                  var newItem = document.getElementById(itemId);
                  $document.duScrollToElementAnimated(angular.element(newItem));
              }, 0);
          }
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('TeacherController', TeacherController);
  
      TeacherController.$inject = [
          'authService', 'usersService',
          '$mdDialog'
      ];
  
      function TeacherController(authService, usersService, $mdDialog) {
  
          var vm = this;
          vm.authService = authService;
  
          vm.busy = false;
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
              var newId = alertId++;
              vm[type].push({
                  alertid : newId,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
              return newId;
          }
  
  
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  if (profile.role === 'supervisor') {
                      usersService.getClassPolicy(profile)
                          .then(function (policy) {
                              vm.policy = policy;
  
                              vm.policy.missingCredentials = false;
                              for (var i = 0; i < policy.supportedProjectTypes.length; i++) {
                                  var projectType = policy.supportedProjectTypes[i];
                                  if ((projectType === 'text' && policy.maxTextModels === 0) ||
                                      (projectType === 'images' && policy.maxImageModels === 0))
                                  {
                                      vm.policy.missingCredentials = true;
                                  }
                              }
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
                          });
                  }
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data);
              });
  
  
  
  
          vm.deleteClass = function (ev) {
              var confirm = $mdDialog.confirm()
                              .title('Are you sure?')
                              .htmlContent('<div class="confirmdialogsmall">This cannot be undone. I will not ' +
                                  'be able to retrieve any of the projects from the class if you do this.</div>')
                              .targetEvent(ev)
                              .ok('Yes. Delete everything.')
                              .cancel('No');
              $mdDialog.show(confirm)
                  .then(
                      function () {
                          vm.busy = true;
                          usersService.deleteClass(vm.profile)
                              .then(function () {
                                  authService.logout();
                              })
                              .catch(function (err) {
                                  vm.busy = false;
                                  displayAlert('errors', err.status, err);
                              });
                      },
                      function () { /* cancelled */ }
                  );
          }
  
  
  
  
      }
  }());
  
  angular.module('app')
      .directive('httpSrc', [
          '$http', function ($http) {
              var directive = {
                  link: link,
                  restrict: 'A'
              };
              return directive;
  
              function link(scope, element, attrs) {
                  var requestConfig = {
                      method: 'Get',
                      url: attrs.httpSrc,
                      responseType: 'arraybuffer',
                      cache: 'true'
                  };
  
                  $http(requestConfig)
                      .then(function(response) {
                          var contentType = response.headers('content-type');
  
                          var arr = new Uint8Array(response.data);
  
                          var raw = '';
                          var i, j, subArray, chunk = 5000;
                          for (i = 0, j = arr.length; i < j; i += chunk) {
                              subArray = arr.subarray(i, i + chunk);
                              raw += String.fromCharCode.apply(null, subArray);
                          }
  
                          var b64 = btoa(raw);
  
                          attrs.$set('src', 'data:' + contentType + ';base64,' + b64);
                      });
              }
          }
      ]);
  
  (function () {
  
      angular
          .module('app')
          .directive('ignoreDrop', ignoreDrop);
  
      /* Students sometimes miss when dragging images into a training bucket for */
      /* handling by mlImageLoader, and accidentally drop the image just outside */
      /* the bucket. This is handled by the browser's default behaviour, which   */
      /* is to navigate to the URL for the dropped image. This has caused        */
      /* confusion in several classes as students don't understand why they've   */
      /* lost their page, or that they need to click 'Back' to return to their   */
      /* training work.                                                          */
      /*                                                                         */
      /* This directive is a way to handle this by capturing and swallowing      */
      /* drag and drop events on divs that hold the training buckets.            */
  
      function ignoreDrop() {
  
          function cancel(evt) {
              if (evt && evt.preventDefault) {
                  evt.preventDefault();
              }
              return false;
          }
  
          function link (scope, jqlElements, attrs) {
              var jqlElement = jqlElements[0];
  
              jqlElement.addEventListener('drop', cancel);
              jqlElement.addEventListener('dragover', cancel);
          }
  
          return {
              link : link
          };
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .directive('mlImageLoader', mlImageLoader);
  
  
      function mlImageLoader() {
  
          function cancel(evt) {
              if (evt && evt.preventDefault) {
                  evt.preventDefault();
              }
              if (evt && evt.dataTransfer) {
                  evt.dataTransfer.dropEffect = 'copy';
              }
              return false;
          }
  
  
          function parseUrl(url) {
              var result = {};
              url.split('&').forEach(function(part) {
                  if (!part) {
                      return;
                  }
  
                  part = part.split('+').join(' ');
  
                  var eq = part.indexOf('=');
                  var key = eq > -1 ? part.substr(0, eq) : part;
                  var val = eq > -1 ? decodeURIComponent(part.substr(eq + 1)) : '';
  
                  var from = key.indexOf('[');
                  if (from === -1) {
                      result[decodeURIComponent(key)] = val;
                  }
                  else {
                      var to = key.indexOf(']', from);
                      var index = decodeURIComponent(key.substring(from + 1, to));
                      key = decodeURIComponent(key.substring(0, from));
  
                      if (!result[key]) {
                          result[key] = [];
                      }
  
                      if (!index) {
                          result[key].push(val);
                      }
                      else {
                          result[key][index] = val;
                      }
                  }
              });
              return result;
          }
  
          function parseHTML(str) {
              var tmp = document.implementation.createHTMLDocument('title');
              tmp.body.innerHTML = str;
              return tmp;
          }
  
  
          // is it an image search result from Google?
          var GOOG_IMG_REGEX = /^https:\/\/www\.google\.co[a-z.]+\/imgres\?(imgurl=.*)/;
  
          // is it an image search result from Baidu?
          var BAIDU_IMG_REGEX = /^https:\/\/timgsa.baidu.com\/timg\?.*/;
          var BAIDU_IMG_SRCH_REGEX = /^https:\/\/image.baidu.com\/search\/detail.*/;
  
          // is it a URL ending with .png or .jpg ?
          var IMG_URL_REGEX = /^https?:\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)(\.jpg|\.png)\??.*$/;
  
          function getType(types, type) {
              if (types && types.indexOf && types.indexOf(type) !== -1) {
                  return type;
              }
              else {
                  return 'Text';
              }
          }
  
  
          function reportInvalidImageType(scope) {
              scope.getController().displayAlert('warnings', 400, {
                  message : 'Sorry, that type of image is not supported. You can only use jpg or png pictures'
              });
              scope.$apply();
              return false;
          }
  
          function urlIsImageData(url) {
              return url && typeof url === 'string' &&
                     (url.substr(0, 10) === 'data:image' ||
                      url.substr(0, 5) === 'blob:' ||
                      url.substr(0, 11) === 'x-raw-image' ||
                      url.substr(0, 5) === 'file:');
          }
  
  
          function handleDrop(evt, label, scope) {
              if (!evt || !evt.dataTransfer) {
                  return false;
              }
  
              if (evt.preventDefault) {
                  evt.preventDefault();
              }
  
              if (evt.dataTransfer &&
                  evt.dataTransfer.types && evt.dataTransfer.types.length === 1 && evt.dataTransfer.types[0] === 'Files')
              {
                  scope.getController().displayAlert('warnings', 400, {
                      message : 'You can\'t upload pictures from your computer. Use pictures that are already online, by dragging them from another web page'
                  });
                  scope.$apply();
                  return false;
              }
  
              var data;
              if (evt.dataTransfer.types && evt.dataTransfer.types.length > 0) {
                  var type = getType(evt.dataTransfer.types, 'text/uri-list');
                  var src = evt.dataTransfer.getData(type);
  
                  if (urlIsImageData(src)) {
                      return reportInvalidImageType(scope);
                  }
                  var googleImagesCheck = src.match(GOOG_IMG_REGEX);
                  if (googleImagesCheck) {
                      var googleImagesUrl = googleImagesCheck[1];
                      var googleImgsUrlParms = parseUrl(googleImagesUrl);
                      data = googleImgsUrlParms.imgurl;
                  }
                  if (!data) {
                      var baiduImagesCheck = src.match(BAIDU_IMG_REGEX);
                      if (baiduImagesCheck) {
                          var baiduUrl = new URL(src);
                          if (baiduUrl &&
                              baiduUrl.searchParams &&
                              baiduUrl.searchParams.has &&
                              baiduUrl.searchParams.has('src'))
                          {
                              data = baiduUrl.searchParams.get('src');
                          }
                      }
                  }
                  if (!data) {
                      var baiduImageSearchCheck = src.match(BAIDU_IMG_SRCH_REGEX);
                      if (baiduImageSearchCheck) {
                          var baiduSearchUrl = new URL(src);
                          if (baiduSearchUrl &&
                              baiduSearchUrl.searchParams &&
                              baiduSearchUrl.searchParams.has &&
                              baiduSearchUrl.searchParams.has('objurl'))
                          {
                              data = baiduSearchUrl.searchParams.get('objurl');
                          }
                      }
                  }
                  if (!data) {
                      var htmltype = getType(evt.dataTransfer.types, 'text/html');
                      var linksrc = evt.dataTransfer.getData(htmltype);
                      var parsed = parseHTML(linksrc);
                      var img = parsed.querySelector('img');
                      if (img) {
                          data = img.src;
                      }
                  }
                  if (!data) {
                      var imageUrlCheck = src.match(IMG_URL_REGEX);
                      if (imageUrlCheck) {
                          data = src;
                      }
                  }
              }
              if (!data) {
                  data = evt.dataTransfer.getData('Text');
              }
  
  
              if (data) {
                  if (urlIsImageData(data)) {
                      return reportInvalidImageType(scope);
                  }
  
                  scope.getController().addConfirmedTrainingData(data, label);
                  scope.$apply();
              }
              else {
                  scope.getController().displayAlert('warnings', 400, {
                      message : 'Drag a picture from another web page into one of the training buckets'
                  });
                  scope.$apply();
              }
  
              return false;
          }
  
  
          function link (scope, jqlElements, attrs) {
              var jqlElement = jqlElements[0];
              var label = scope.$parent.label;
  
              jqlElement.addEventListener('dragover', cancel);
  
              var counter = 0;
  
              jqlElement.addEventListener('dragleave', function (evt) {
                  counter -= 1;
                  if (counter === 0) {
                      angular.element(evt.target).removeClass('hover');
                  }
                  return cancel(evt);
              });
              jqlElement.addEventListener('dragenter', function (evt) {
                  counter += 1;
                  angular.element(evt.target).addClass('hover');
                  return cancel(evt);
              });
              jqlElement.addEventListener('drop', function (evt) {
                  if (evt && evt.dataTransfer) {
                      counter = 0;
                      angular.element(evt.target).removeClass('hover');
                      jqlElements.removeClass('hover');
  
                      angular.element(document.querySelector('.trainingbucketitems.hover')).removeClass('hover');
  
                      return handleDrop(evt, label, scope);
                  }
              });
          }
  
          return {
              link : link
          };
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .directive('mlImageOnError', mlImageOnError);
  
      function mlImageOnError() {
  
          function link (scope, jqlElements, attrs) {
              jqlElements.bind('error', function () {
                  scope.$apply(attrs.mlImageOnError);
              });
          }
  
          return {
              link : link
          };
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .controller('TrainingController', TrainingController);
  
      TrainingController.$inject = [
          'authService',
          'projectsService', 'trainingService',
          'soundTrainingService',
          '$stateParams',
          '$scope',
          '$mdDialog',
          '$state',
          '$timeout',
          '$q'
      ];
  
      function TrainingController(authService, projectsService, trainingService, soundTrainingService, $stateParams, $scope, $mdDialog, $state, $timeout, $q) {
  
          var vm = this;
          vm.authService = authService;
  
          var placeholderId = 1;
  
  
          var alertId = 1;
          vm.errors = [];
          vm.warnings = [];
          vm.dismissAlert = function (type, errIdx) {
              vm[type].splice(errIdx, 1);
          };
          function displayAlert(type, status, errObj) {
              if (!errObj) {
                  errObj = {};
              }
              else {
                  // record the error
                  console.log(errObj);
                  if (status === 500 && Sentry && Sentry.captureException) {
                      Sentry.captureException({ error : errObj, errortype : typeof (errObj) });
                  }
              }
  
              vm[type].push({
                  alertid : alertId++,
                  message : errObj.message || errObj.error || 'Unknown error',
                  status : status
              });
          }
          vm.displayAlert = displayAlert;
  
          $scope.loadingtraining = true;
  
          $scope.crowdSourced = false;
  
          $scope.projectId = $stateParams.projectId;
          $scope.userId = $stateParams.userId;
          $scope.training = {};
  
  
  
          // check that they're authenticated before doing anything else
          authService.getProfileDeferred()
              .then(function (profile) {
                  vm.profile = profile;
  
                  // get the project that we're going to be training
                  return projectsService.getProject($scope.projectId, $scope.userId, profile.tenant);
              })
              .then(function (project) {
                  $scope.project = project;
  
                  // if the user doesn't own the project (it's been shared with them by a teacher
                  //  using the "crowd-sourced" mode) then we need to hide some of the controls
                  $scope.crowdSourced = project.isCrowdSourced &&
                                        (vm.profile.user_id !== project.userid);
  
                  // for some projects we need to fetch some more things...
  
                  if (project.type === 'numbers') {
                      // for numbers projects we need the fields to populate the drop-downs for new values
                      return projectsService.getFields($scope.projectId, $scope.userId, vm.profile.tenant)
                          .then(function (fields) {
                              $scope.project.fields = fields;
                          });
                  }
                  else if (project.type === 'sounds') {
                      // for sounds projects we need to download the TensorFlow.js libraries if we don't
                      //  already have them in the page
                      return soundTrainingService.initSoundSupport(project.id)
                          .then(function () {
                              $scope.soundModelInfo = soundTrainingService.getModelInfo();
                          });
                  }
              })
              .then(function () {
                  // we should have everything we need to prepare the page header now
                  refreshLabelsSummary();
  
                  // prepare the empty training buckets for the project
                  for (var labelIdx in $scope.project.labels) {
                      var label = $scope.project.labels[labelIdx];
                      $scope.training[label] = [];
                  }
  
                  // fetch the training data to populate the buckets with
                  return trainingService.getTraining($scope.projectId, $scope.userId, vm.profile.tenant);
              })
              .then(function (training) {
                  // all the training data items will be returned in one list
                  //  so they need to be sorted into the different buckets now
                  for (var trainingitemIdx in training) {
                      var trainingitem = training[trainingitemIdx];
  
                      var label = trainingitem.label;
  
                      if (label in $scope.training === false) {
                          // This shouldn't happen - it means that we got some training data
                          //  for a label that isn't known to the project.
                          //
                          // It means the page state is out of date (e.g. the label was created
                          //  after the page was first loaded from another instance of the page)
                          //  which is a super unlikely race condition, but we avoid the possible
                          //  error by creating a new bucket with this new label
                          $scope.training[label] = [];
                      }
  
                      $scope.training[label].push(trainingitem);
  
                      // if this is a text project...
                      //          trainingitem has the complete data - nothing left to do
                      // if this is a numbers project...
                      //          trainingitem has the complete data - nothing left to do
                      // if this is an images project...
                      //          trainingitem has the URL for the image, but the browser will fetch it
                      //              for us automatically when we put it in the img src attribute
                      //              so nothing left to do in code here, but there will be another
                      //              network request before the image appears in the UI
                      // if this is a sounds project...
                      //          trainingitem has the URL for the sound spectogram, but we need to
                      //              explicitly fetch it now (the page will display a loading icon
                      //              until we get it)
  
  
  
                      if ($scope.project.type === 'sounds') {
                          // this will modify 'trainingitem' to add a 'audiodata' attribute
                          //  (but not immediately as it'll need to make an XHR request to get it)
                          trainingService.getSoundData(trainingitem);
                      }
                  }
  
                  $scope.loadingtraining = false;
              })
              .catch(function (err) {
                  displayAlert('errors', err.status, err.data ? err.data : err);
              });
  
  
          function refreshLabelsSummary () {
              var summary = '';
              if ($scope.project.labels.length > 0) {
                  var labels = $scope.project.type === 'sounds' ?
                      $scope.project.labels.filter(function (label) {
                          return label !== '_background_noise_';
                      }) :
                      $scope.project.labels;
  
                  switch (labels.length) {
                      case 0:
                          summary = '';
                          break;
                      case 1:
                          summary = labels[0];
                          break;
                      case 2:
                          summary = labels[0] + ' or ' + labels[1];
                          break;
                      case 3:
                          summary = labels[0] + ', ' +
                                    labels[1] + ' or ' +
                                    labels[2];
                          break;
                      default:
                          summary = labels[0] + ', ' +
                                    labels[1] + ' or ' +
                                    (labels.length - 2) + ' other classes';
                          break;
                  }
              }
              $scope.project.labelsSummary = summary;
          }
  
  
          function getValues(obj) {
              return Object.keys(obj).map(function (key) {
                  return obj[key];
              });
          }
  
          vm.addTrainingData = function (ev, label) {
              $mdDialog.show({
                  locals : {
                      label : label,
                      project : $scope.project
                  },
                  controller : function ($scope, locals) {
                      $scope.label = locals.label;
                      $scope.project = locals.project;
                      $scope.values = {};
  
                      $scope.hide = function() {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function() {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function(resp) {
                          $mdDialog.hide(resp);
                      };
                      $scope.submitOnEnter = function(event) {
                          var code = event.keyCode || event.which;
                          if (code === 13) {
                              event.preventDefault();
                              $scope.confirm($scope.example);
                          }
                      };
  
                      $scope.$watch('example', function (newval, oldval) {
                          if ($scope && $scope.example && newval !== oldval) {
                              $scope.example = newval.replace(/[\r\n\t]/g, ' ');
                          }
                      }, true);
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/training/trainingdata.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (resp) {
                      vm.addConfirmedTrainingData(resp, label);
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
          vm.addConfirmedTrainingData = function (resp, label) {
  
              var data;
              var placeholder;
  
              var duplicate = false;
  
              var storeTrainingDataFn = trainingService.newTrainingData;
  
              if ($scope.project.type === 'text') {
                  data = resp;
  
                  duplicate = $scope.training[label].some(function (existingitem) {
                      return existingitem.textdata === data;
                  });
  
                  placeholder = {
                      id : placeholderId++,
                      label : label,
                      projectid : $scope.projectId,
                      textdata : data,
                      isPlaceholder : true
                  };
              }
              else if ($scope.project.type === 'numbers') {
                  data = getValues(resp);
  
                  placeholder = {
                      id : placeholderId++,
                      label : label,
                      projectid : $scope.projectId,
                      numberdata : data,
                      isPlaceholder : true
                  };
              }
              else if ($scope.project.type === 'images') {
                  data = resp;
  
                  duplicate = $scope.training[label].some(function (existingitem) {
                      return existingitem.imageurl === data;
                  });
  
                  placeholder = {
                      id : placeholderId++,
                      label : label,
                      projectid : $scope.projectId,
                      imageurl : data,
                      isPlaceholder : true
                  };
              }
              else if ($scope.project.type === 'sounds') {
                  // convert the Float32Array we get from the dialog
                  //  into a regular old JavaScript array
                  // (could use Array.from(resp) but IE doesnt like it)
                  data = Array.prototype.slice.call(resp);
  
                  // duplicates are super unlikely so we're not going to
                  //  waste time checking
  
                  placeholder = {
                      id : placeholderId++,
                      label : label,
                      projectid : $scope.projectId,
                      audiodata : data,
                      isPlaceholder : true
                  };
  
                  // IMPORTANT - we use a different API for uploading sound
                  storeTrainingDataFn = trainingService.uploadSound;
              }
  
              if (duplicate) {
                  return displayAlert('errors', 400, {
                      message : 'That is already in your training data'
                  });
              }
  
              $scope.training[label].push(placeholder);
  
              storeTrainingDataFn($scope.projectId, $scope.userId, vm.profile.tenant, data, label)
                  .then(function (newitem) {
                      placeholder.isPlaceholder = false;
                      placeholder.id = newitem.id;
  
                      scrollToNewItem(newitem.id);
                  })
                  .catch(function (err) {
                      if (errorSuggestsProjectDeleted(err)) {
                          return $state.go('projects');
                      }
  
                      displayAlert('errors', err.status, err.data);
  
                      var idxToRemove = findTrainingIndex(label, placeholder.id);
                      if (idxToRemove !== -1) {
                          $scope.training[label].splice(idxToRemove, 1);
                      }
                  });
          };
  
  
          function errorSuggestsProjectDeleted(err) {
              return err &&
                     err.status === 404 &&
                     err.data &&
                     err.data.error === 'Not found';
          }
  
  
          vm.onImageLoad = function (image) {
              console.log(image);
              console.log('on image load');
          };
  
          vm.onImageError = function (image) {
              image.loadingFailed = true;
              // displayAlert('errors', 400, {
              //     error : 'Image (' + image.imageurl + ') in the ' + image.label + ' bucket could not be loaded, and has been highlighted in red. You should delete it.'
              // });
          };
  
  
          vm.addLabel = function (ev) {
              $mdDialog.show({
                  controller : function ($scope, $mdDialog) {
                      $scope.hide = function () {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function () {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function (resp) {
                          $mdDialog.hide(resp);
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/training/newlabel.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (newlabel) {
                      projectsService.addLabelToProject($scope.projectId, $scope.userId, vm.profile.tenant, newlabel)
                          .then(function (labels) {
                              $scope.project.labels = labels;
                              for (var i = 0; i < labels.length; i++) {
                                  if (!$scope.training[labels[i]]){
                                      $scope.training[labels[i]] = [];
                                  }
                              }
  
                              refreshLabelsSummary();
                          })
                          .catch(function (err) {
                              if (errorSuggestsProjectDeleted(err)) {
                                  return $state.go('projects');
                              }
  
                              displayAlert('errors', err.status, err.data);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
          vm.deleteText = function (label, item, idx) {
              $scope.training[label].splice(idx, 1);
              trainingService.deleteTrainingData($scope.projectId, $scope.userId, vm.profile.tenant, item.id);
          };
  
          vm.deleteLabel = function (ev, label, idx) {
              var confirm = $mdDialog.confirm()
                  .title('Are you sure?')
                  .textContent('Do you want to delete "' + label + '"? (This cannot be undone)')
                  .ariaLabel('Confirm')
                  .targetEvent(ev)
                  .ok('Yes')
                  .cancel('No');
  
              $mdDialog.show(confirm).then(
                  function() {
                      delete $scope.training[label];
                      $scope.project.labels.splice(idx, 1);
  
                      refreshLabelsSummary();
  
                      projectsService.removeLabelFromProject($scope.projectId, $scope.userId, vm.profile.tenant, label)
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
          vm.useWebcam = function (ev, label) {
              $mdDialog.show({
                  locals : {
                      label : label,
                      project : $scope.project
                  },
                  controller : function ($scope, locals) {
                      $scope.label = locals.label;
                      $scope.project = locals.project;
                      $scope.values = {};
                      $scope.channel = {};
                      $scope.webcamerror = false;
                      $scope.webcamInitComplete = false;
  
                      $scope.webcamCanvas = null;
  
                      $scope.hide = function() {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function() {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function() {
                          getWebcamData()
                              .then(function (imagedata) {
                                  $mdDialog.hide(imagedata);
                              });
                      };
  
  
                      $scope.onWebcamSuccess = function () {
                          $scope.$apply(function() {
                              $scope.webcamInitComplete = true;
                          });
                      };
  
                      function displayWebcamError(err) {
                          $scope.webcamerror = err;
                          if (err && err.message) {
                              if (err.name === 'NotAllowedError') {
                                  $scope.webcamerrordetail = 'Not allowed to use the web-cam';
                              }
                              else {
                                  $scope.webcamerrordetail = err.message;
                              }
                          }
                      }
  
                      $scope.onWebcamError = function(err) {
                          $scope.webcamInitComplete = true;
  
                          try {
                              $scope.$apply(
                                  function() {
                                      displayWebcamError(err);
                                  }
                              );
                          }
                          catch (applyErr) {
                              $timeout(function () {
                                  displayWebcamError(err);
                              }, 0, false);
                          }
                      };
  
  
                      function getWebcamData() {
                          var hiddenCanvas = document.createElement('canvas');
                          hiddenCanvas.width = $scope.channel.video.width;
                          hiddenCanvas.height = $scope.channel.video.height;
  
                          var ctx = hiddenCanvas.getContext('2d');
                          ctx.drawImage($scope.channel.video,
                              0, 0,
                              $scope.channel.video.width, $scope.channel.video.height);
  
                          return $q(function(resolve, reject) {
                              hiddenCanvas.toBlob(function (blob) {
                                  resolve(blob);
                              }, 'image/jpeg');
                          });
                      };
  
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/training/webcam.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (resp) {
                      var placeholder = {
                          id : placeholderId++,
                          label : label,
                          projectid: $scope.projectId,
                          imageurl : URL.createObjectURL(resp),
                          isPlaceholder : true
                      };
  
                      $scope.training[label].push(placeholder);
  
                      trainingService.uploadImage($scope.project.id, $scope.userId, vm.profile.tenant, resp, label)
                          .then(function (newitem) {
                              placeholder.isPlaceholder = false;
                              placeholder.id = newitem.id;
  
                              scrollToNewItem(newitem.id);
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
  
                              var idxToRemove = findTrainingIndex(label, placeholder.id);
                              if (idxToRemove !== -1) {
                                  $scope.training[label].splice(idxToRemove, 1);
                              }
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
  
          vm.useCanvas = function (ev, label) {
              $mdDialog.show({
                  locals : {
                      label : label,
                      project : $scope.project
                  },
                  controller : function ($scope, locals) {
                      $scope.label = locals.label;
                      $scope.project = locals.project;
                      $scope.values = {};
  
                      $scope.hide = function() {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function() {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function() {
                          getCanvasData()
                              .then(function (imagedata) {
                                  $mdDialog.hide(imagedata);
                              });
                      };
  
  
                      function getCanvasData() {
                          return $q(function(resolve, reject) {
                              $scope.canvas.toBlob(function (blob) {
                                  resolve(blob);
                              }, 'image/jpeg');
                          });
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/training/canvas.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (resp) {
                      var placeholder = {
                          id : placeholderId++,
                          label : label,
                          projectid: $scope.projectId,
                          imageurl : URL.createObjectURL(resp),
                          isPlaceholder : true
                      };
  
                      $scope.training[label].push(placeholder);
  
                      trainingService.uploadImage($scope.project.id, $scope.userId, vm.profile.tenant, resp, label)
                          .then(function (newitem) {
                              placeholder.isPlaceholder = false;
                              placeholder.id = newitem.id;
  
                              scrollToNewItem(newitem.id);
                          })
                          .catch(function (err) {
                              displayAlert('errors', err.status, err.data);
  
                              var idxToRemove = findTrainingIndex(label, placeholder.id);
                              if (idxToRemove !== -1) {
                                  $scope.training[label].splice(idxToRemove, 1);
                              }
                          });
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
  
          vm.useMicrophone = function (ev, label) {
              $mdDialog.show({
                  locals : {
                      label : label,
                      project : $scope.project,
                      soundModelInfo : soundTrainingService.getModelInfo(),
                  },
                  controller : function ($scope, locals) {
                      $scope.label = locals.label;
                      $scope.project = locals.project;
                      $scope.soundModelInfo = locals.soundModelInfo;
                      $scope.values = {};
  
                      $scope.hide = function() {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function() {
                          $mdDialog.cancel();
                      };
                      $scope.confirm = function(resp) {
                          $mdDialog.hide(resp);
                      };
  
                      $scope.recordSound = function(label) {
                          delete $scope.example;
                          $scope.recording = true;
  
                          $scope.recordingprogress = 0;
                          var progressInterval = setInterval(function () {
                              $scope.$apply(
                                  function() {
                                      $scope.recordingprogress += 10;
                                  });
                          }, 1000 / 10);
  
                          soundTrainingService.collectExample(label)
                              .then(function (spectogram) {
                                  clearInterval(progressInterval);
                                  $scope.$apply(
                                      function() {
                                          $scope.recordingprogress = 100;
                                          if (spectogram && spectogram.data && spectogram.data.length > 0) {
                                              $scope.example = spectogram.data;
                                          }
                                          $scope.recording = false;
                                      });
                              })
                              .catch(function () {
                                  clearInterval(progressInterval);
                                  $scope.recording = false;
                              });
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/training/trainingdata.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              })
              .then(
                  function (resp) {
                      vm.addConfirmedTrainingData(resp, label);
                  },
                  function() {
                      // cancelled. do nothing
                  }
              );
          };
  
  
          function scrollToNewItem(itemId) {
              $timeout(function () {
                  var newItem = document.getElementById(itemId);
                  var itemContainer = newItem.parentElement;
                  angular.element(itemContainer).duScrollToElementAnimated(angular.element(newItem));
              }, 0);
          }
  
  
  
          function findTrainingIndex(label, id) {
              var len = $scope.training[label].length;
              for (var i = 0; i < len; i++) {
                  if ($scope.training[label][i].id === id) {
                      return i;
                  }
              }
              return -1;
          }
  
  
          $scope.getController = function() {
              return vm;
          };
      }
  }());
  
  (function () {
  
      angular
          .module('app')
          .service('trainingService', trainingService);
  
      trainingService.$inject = [
          '$q', '$http'
      ];
  
      function trainingService($q, $http) {
  
  
          function newTrainingData(projectid, userid, tenant, data, label) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/training';
  
              return $http.post(url, { data : data, label : label })
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function deleteTrainingData(projectid, userid, tenant, trainingdataid) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/training/' + trainingdataid;
  
              return $http.delete(url);
          }
  
          function getTraining(projectid, userid, tenant) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/training';
  
              return $http.get(url, { headers : { Range : 'items=0-3000' } })
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function getSoundData(soundobj) {
              return $http.get(soundobj.audiourl)
                  .then(function (resp) {
                      soundobj.audiodata = resp.data;
                      return soundobj;
                  });
          }
  
          function getModels(projectid, userid, tenant) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/models';
  
              return $http.get(url).then(function (resp) {
                  var models = resp.data;
                  if (models) {
                      var now = new Date();
                      for (var i = 0; i < models.length; i++) {
                          models[i].lastPollTime = now;
                      }
                  }
                  return models;
              });
          }
  
          function getModel(projectid, userid, tenant, modelid, timestamp) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/models/' + modelid +
                          '?ts=' + timestamp;
  
               return $http.get(url).then(function (resp) {
                  return resp.data;
              });
          }
  
          function newModel(projectid, userid, tenant) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/models';
  
              return $http.post(url, {}, { timeout : 180000 }).then(function (resp) {
                  resp.data.lastPollTime = new Date();
                  return resp.data;
              });
          }
  
          function testModel(projectid, projecttype, userid, tenant, modelid, credsid, testdata) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/models/' + modelid +
                          '/label';
              testdata.credentialsid = credsid;
  
              return $http.post(url, testdata)
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function deleteModel(projectid, userid, tenant, modelid) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/models/' + modelid;
  
              return $http.delete(url);
          }
  
  
          function uploadImage(projectid, userid, tenant, imgdata, label) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/images';
  
              var data = new FormData();
              data.append('image', imgdata, 'webcam.jpg');
              data.append('label', label);
  
              var postreq = {
                  transformRequest: angular.identity,
                  headers: { 'Content-Type': undefined }
              };
  
              return $http.post(url, data, postreq)
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function uploadSound(projectid, userid, tenant, audiodata, label) {
              var url = '/api/classes/' + tenant +
                          '/students/' + userid +
                          '/projects/' + projectid +
                          '/sounds';
  
              return $http.post(url, { data : audiodata, label : label })
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
  
  
          function getUnmanagedClassifiers(tenant) {
              var url = '/api/classes/' + tenant + '/classifiers';
  
              return $http.get(url, { params : { type : 'unmanaged' } })
                  .then(function (resp) {
                      return resp.data;
                  });
          }
  
          function deleteBluemixClassifier(tenant, classifierid, credentialsid, type) {
              var url = '/api/classes/' + tenant + '/classifiers/' + classifierid;
  
              return $http.delete(url, {
                  params : {
                      type : type,
                      credentialsid : credentialsid
                  }
              });
          }
  
  
          return {
              newTrainingData : newTrainingData,
              uploadImage : uploadImage,
  
              uploadSound : uploadSound,
              getSoundData : getSoundData,
  
              getTraining : getTraining,
              deleteTrainingData : deleteTrainingData,
              getModels : getModels,
              getModel : getModel,
              newModel : newModel,
              testModel : testModel,
              deleteModel : deleteModel,
  
              getUnmanagedClassifiers : getUnmanagedClassifiers,
              deleteBluemixClassifier : deleteBluemixClassifier
          };
      }
  })();
  
  (function () {
  
      angular
          .module('app')
          .service('usersService', usersService);
  
      usersService.$inject = [
          '$q', '$http'
      ];
  
  
      function usersService($q, $http) {
  
          function returnData(resp) {
              return resp.data;
          }
  
          function getClassPolicy(profile) {
              return $http.get('/api/classes/' + profile.tenant + '/policy')
                  .then(returnData);
          }
  
          function modifyClassPolicy(profile, textexpiry, imageexpiry) {
              var modification = [
                  { op : 'replace', path : '/textClassifierExpiry', value : textexpiry },
                  { op : 'replace', path : '/imageClassifierExpiry', value : imageexpiry }
              ];
              return $http.patch('/api/classes/' + profile.tenant + '/policy', modification)
                  .then(returnData);
          }
  
  
          function createTeacher(username, email, notes) {
              var newteacher = {
                  username : username,
                  email : email,
                  notes : notes
              };
  
              return $http.post('/api/teachers', newteacher)
                  .then(returnData);
          }
  
  
          function deleteClass(profile) {
              return $http.delete('/api/classes/' + profile.tenant + '?confirm=true');
          }
  
          function getStudentList(profile) {
              return $http.get('/api/classes/' + profile.tenant + '/students')
                  .then(returnData);
          }
  
          function deleteStudent(profile, tenant) {
              return $http.delete('/api/classes/' + tenant + '/students/' + profile.id);
          }
  
          function createStudent(username, tenant) {
              var newstudent = {
                  username : username
              };
  
              return $http.post('/api/classes/' + tenant + '/students', newstudent)
                  .then(returnData);
          }
  
          function createStudents(tenant, prefix, number, password) {
              var bulkCreate = {
                  prefix : prefix,
                  number : number,
                  password : password
              };
              return $http.put('/api/classes/' + tenant + '/students', bulkCreate)
                  .then(returnData);
          }
  
          function resetStudentPassword(profile, tenant) {
              return $http.post('/api/classes/' + tenant + '/students/' + profile.id + '/password')
                  .then(returnData);
          }
          function resetStudentsPassword(profiles, tenant) {
              var students = profiles.map(function (profile) {
                  return {
                      op : 'replace',
                      path : '/password',
                      value : { id : profile.id }
                  };
              });
              return $http.patch('/api/classes/' + tenant + '/students', students)
                  .then(returnData);
          }
  
  
          function getGeneratedPassword(tenant) {
              return $http.get('/api/classes/' + tenant + '/passwords')
                  .then(returnData);
          }
  
  
  
          function getCredentials(profile, type) {
              return $http.get('/api/classes/' + profile.tenant + '/credentials?servicetype=' + type)
                  .then(returnData);
          }
          function deleteCredentials(profile, credentials) {
              return $http.delete('/api/classes/' + profile.tenant + '/credentials/' + credentials.id);
          }
          function addCredentials(credentials, tenant) {
              return $http.post('/api/classes/' + tenant + '/credentials', credentials)
                  .then(returnData);
          }
          function modifyCredentials(credentials, servicetype, credstype, tenant) {
              var update = [
                  {
                      op : 'replace',
                      path : '/credstype',
                      value : {
                          servicetype : servicetype,
                          credstype : credstype
                      }
                  }
              ];
              return $http.patch('/api/classes/' + tenant + '/credentials/' + credentials.id, update)
                  .then(returnData);
          }
  
  
          return {
              createTeacher : createTeacher,
  
              addCredentials : addCredentials,
              getCredentials : getCredentials,
              modifyCredentials : modifyCredentials,
              deleteCredentials : deleteCredentials,
  
              getGeneratedPassword : getGeneratedPassword,
  
              getClassPolicy : getClassPolicy,
              modifyClassPolicy : modifyClassPolicy,
  
              getStudentList : getStudentList,
  
              createStudent : createStudent,
              createStudents : createStudents,
              deleteStudent : deleteStudent,
  
              resetStudentPassword : resetStudentPassword,
              resetStudentsPassword : resetStudentsPassword,
  
  
              deleteClass : deleteClass
          };
      }
  })();
  
  (function () {
  
      angular
          .module('app')
          .service('utilService', utilService);
  
      utilService.$inject = [
          '$q'
      ];
  
      function utilService($q) {
  
          function loadScript(url) {
              return $q(function (resolve, reject) {
                  var id = 'mlforkids-script-' + url;
                  if (document.getElementById(id)) {
                      resolve();
                  }
                  else {
                      var scriptObj = document.createElement('script');
                      scriptObj.id = id;
                      scriptObj.type = 'text/javascript';
                      scriptObj.src = url;
  
                      scriptObj.onreadystatechange = resolve;
                      scriptObj.onload = resolve;
                      scriptObj.onerror = reject;
  
                      document.head.appendChild(scriptObj);
                  }
              });
          }
  
          function isInternetExplorer() {
              var userAgent = navigator.userAgent;
              return userAgent &&
                     (userAgent.indexOf('MSIE') >= 0 || userAgent.indexOf('Trident') >= 0);
          }
  
  
  
          return {
              loadScript : loadScript,
              isInternetExplorer : isInternetExplorer
          };
      }
  
  })();
  
  (function () {
  
      angular
          .module('app')
          .controller('WorksheetsController', WorksheetsController);
  
      WorksheetsController.$inject = [
          '$stateParams',
          '$translate',
          '$mdDialog', '$scope'
      ];
  
      function WorksheetsController($stateParams, $translate, $mdDialog, $scope) {
  
          var vm = this;
  
          vm.worksheets = [];
          $translate([
              'WORKSHEETS.SMARTCLASSROOM.TITLE', 'WORKSHEETS.SMARTCLASSROOM.SUMMARY', 'WORKSHEETS.SMARTCLASSROOM.DESCRIPTION',
              'WORKSHEETS.SMARTCLASSROOM.WORKSHEET_1.DESCRIPTION', 'WORKSHEETS.SMARTCLASSROOM.WORKSHEET_1.URL',
              'WORKSHEETS.SMARTCLASSROOM.WORKSHEET_2.DESCRIPTION', 'WORKSHEETS.SMARTCLASSROOM.WORKSHEET_2.URL',
              'WORKSHEETS.SMARTCLASSROOM.WORKSHEET_3.DESCRIPTION', 'WORKSHEETS.SMARTCLASSROOM.WORKSHEET_3.URL',
  
              'WORKSHEETS.MAKEMEHAPPY.TITLE', 'WORKSHEETS.MAKEMEHAPPY.SUMMARY', 'WORKSHEETS.MAKEMEHAPPY.DESCRIPTION',
              'WORKSHEETS.MAKEMEHAPPY.WORKSHEET_1.DESCRIPTION', 'WORKSHEETS.MAKEMEHAPPY.WORKSHEET_1.URL',
              'WORKSHEETS.MAKEMEHAPPY.WORKSHEET_2.DESCRIPTION', 'WORKSHEETS.MAKEMEHAPPY.WORKSHEET_2.URL',
              'WORKSHEETS.MAKEMEHAPPY.WORKSHEET_3.DESCRIPTION', 'WORKSHEETS.MAKEMEHAPPY.WORKSHEET_3.URL',
  
              'WORKSHEETS.SNAP.TITLE', 'WORKSHEETS.SNAP.SUMMARY', 'WORKSHEETS.SNAP.DESCRIPTION',
              'WORKSHEETS.SNAP.WORKSHEET_1.DESCRIPTION', 'WORKSHEETS.SNAP.WORKSHEET_1.URL',
              'WORKSHEETS.SNAP.WORKSHEET_2.DESCRIPTION', 'WORKSHEETS.SNAP.WORKSHEET_2.URL', 'WORKSHEETS.SNAP.WORKSHEET_2.RESOURCES',
  
              'WORKSHEETS.CHAMELEON.TITLE', 'WORKSHEETS.CHAMELEON.SUMMARY', 'WORKSHEETS.CHAMELEON.DESCRIPTION',
              'WORKSHEETS.CHAMELEON.WORKSHEET_1.URL',
  
              'WORKSHEETS.MAILMANMAX.TITLE', 'WORKSHEETS.MAILMANMAX.SUMMARY', 'WORKSHEETS.MAILMANMAX.DESCRIPTION',
              'WORKSHEETS.MAILMANMAX.WORKSHEET_1.URL',
  
              'WORKSHEETS.CARORCUP.TITLE', 'WORKSHEETS.CARORCUP.SUMMARY', 'WORKSHEETS.CARORCUP.DESCRIPTION',
              'WORKSHEETS.CARORCUP.WORKSHEET_1.DESCRIPTION', 'WORKSHEETS.CARORCUP.WORKSHEET_1.URL',
              'WORKSHEETS.CARORCUP.WORKSHEET_2.DESCRIPTION', 'WORKSHEETS.CARORCUP.WORKSHEET_2.URL',
  
              'WORKSHEETS.FACELOCK.TITLE', 'WORKSHEETS.FACELOCK.SUMMARY', 'WORKSHEETS.FACELOCK.DESCRIPTION',
              'WORKSHEETS.FACELOCK.WORKSHEET_1.URL',
  
              'WORKSHEETS.JOURNEYTOSCHOOL.TITLE', 'WORKSHEETS.JOURNEYTOSCHOOL.SUMMARY', 'WORKSHEETS.JOURNEYTOSCHOOL.DESCRIPTION',
              'WORKSHEETS.JOURNEYTOSCHOOL.WORKSHEET_1.URL',
  
              'WORKSHEETS.SHYPANDA.TITLE', 'WORKSHEETS.SHYPANDA.SUMMARY', 'WORKSHEETS.SHYPANDA.DESCRIPTION',
              'WORKSHEETS.SHYPANDA.WORKSHEET_1.URL',
  
              'WORKSHEETS.ALIENLANGUAGE.TITLE', 'WORKSHEETS.ALIENLANGUAGE.SUMMARY', 'WORKSHEETS.ALIENLANGUAGE.DESCRIPTION',
              'WORKSHEETS.ALIENLANGUAGE.WORKSHEET_1.URL',
  
              'WORKSHEETS.SECRETCODE.TITLE', 'WORKSHEETS.SECRETCODE.SUMMARY', 'WORKSHEETS.SECRETCODE.DESCRIPTION',
              'WORKSHEETS.SECRETCODE.WORKSHEET_1.URL',
  
              'WORKSHEETS.VIRTUALPET.TITLE', 'WORKSHEETS.VIRTUALPET.SUMMARY', 'WORKSHEETS.VIRTUALPET.DESCRIPTION',
              'WORKSHEETS.VIRTUALPET.WORKSHEET_1.URL',
  
              'WORKSHEETS.PACMAN.TITLE', 'WORKSHEETS.PACMAN.SUMMARY', 'WORKSHEETS.PACMAN.DESCRIPTION',
              'WORKSHEETS.PACMAN.WORKSHEET_1.URL',
  
              'WORKSHEETS.CHATBOTS.TITLE', 'WORKSHEETS.CHATBOTS.SUMMARY', 'WORKSHEETS.CHATBOTS.DESCRIPTION',
              'WORKSHEETS.CHATBOTS.WORKSHEET_1.DESCRIPTION', 'WORKSHEETS.CHATBOTS.WORKSHEET_1.URL',
              'WORKSHEETS.CHATBOTS.WORKSHEET_2.DESCRIPTION', 'WORKSHEETS.CHATBOTS.WORKSHEET_2.URL',
  
              'WORKSHEETS.TOURISTINFO.TITLE', 'WORKSHEETS.TOURISTINFO.SUMMARY', 'WORKSHEETS.TOURISTINFO.DESCRIPTION',
              'WORKSHEETS.TOURISTINFO.WORKSHEET_1.URL',
  
              'WORKSHEETS.SORTINGHAT.TITLE', 'WORKSHEETS.SORTINGHAT.SUMMARY', 'WORKSHEETS.SORTINGHAT.DESCRIPTION',
              'WORKSHEETS.SORTINGHAT.WORKSHEET_1.DESCRIPTION', 'WORKSHEETS.SORTINGHAT.WORKSHEET_1.URL',
              'WORKSHEETS.SORTINGHAT.WORKSHEET_2.DESCRIPTION', 'WORKSHEETS.SORTINGHAT.WORKSHEET_2.URL',
  
              'WORKSHEETS.ROCKPAPERSCISSORS.TITLE', 'WORKSHEETS.ROCKPAPERSCISSORS.SUMMARY', 'WORKSHEETS.ROCKPAPERSCISSORS.DESCRIPTION',
              'WORKSHEETS.ROCKPAPERSCISSORS.WORKSHEET_1.URL',
  
              'WORKSHEETS.JUDGEABOOK.TITLE', 'WORKSHEETS.JUDGEABOOK.SUMMARY', 'WORKSHEETS.JUDGEABOOK.DESCRIPTION',
              'WORKSHEETS.JUDGEABOOK.WORKSHEET_1.DESCRIPTION', 'WORKSHEETS.JUDGEABOOK.WORKSHEET_1.URL',
              'WORKSHEETS.JUDGEABOOK.WORKSHEET_2.DESCRIPTION', 'WORKSHEETS.JUDGEABOOK.WORKSHEET_2.URL',
  
              'WORKSHEETS.LOCATELARRY.TITLE', 'WORKSHEETS.LOCATELARRY.SUMMARY', 'WORKSHEETS.LOCATELARRY.DESCRIPTION',
              'WORKSHEETS.LOCATELARRY.WORKSHEET_1.URL',
  
              'WORKSHEETS.CONFUSED.TITLE', 'WORKSHEETS.CONFUSED.SUMMARY', 'WORKSHEETS.CONFUSED.DESCRIPTION',
              'WORKSHEETS.CONFUSED.WORKSHEET_1.URL',
  
              'WORKSHEETS.SCHOOLLIBRARY.TITLE', 'WORKSHEETS.SCHOOLLIBRARY.SUMMARY', 'WORKSHEETS.SCHOOLLIBRARY.DESCRIPTION',
              'WORKSHEETS.SCHOOLLIBRARY.WORKSHEET_1.URL',
  
              'WORKSHEETS.WHATTWITTERTHINKS.TITLE', 'WORKSHEETS.WHATTWITTERTHINKS.SUMMARY', 'WORKSHEETS.WHATTWITTERTHINKS.DESCRIPTION',
              'WORKSHEETS.WHATTWITTERTHINKS.WORKSHEET_1.URL',
  
              'WORKSHEETS.NOUGHTSANDCROSSES.TITLE', 'WORKSHEETS.NOUGHTSANDCROSSES.SUMMARY', 'WORKSHEETS.NOUGHTSANDCROSSES.DESCRIPTION',
              'WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_1.DESCRIPTION', 'WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_1.URL',
              'WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_2.DESCRIPTION', 'WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_2.URL',
              'WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_3.DESCRIPTION', 'WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_3.URL',
  
              'WORKSHEETS.TOPTRUMPS.TITLE', 'WORKSHEETS.TOPTRUMPS.SUMMARY', 'WORKSHEETS.TOPTRUMPS.DESCRIPTION',
              'WORKSHEETS.TOPTRUMPS.WORKSHEET_1.URL',
  
              'WORKSHEETS.NEWSPAPERSHELVES.TITLE', 'WORKSHEETS.NEWSPAPERSHELVES.SUMMARY', 'WORKSHEETS.NEWSPAPERSHELVES.DESCRIPTION',
              'WORKSHEETS.NEWSPAPERSHELVES.WORKSHEET_1.URL',
  
              'WORKSHEETS.HEADLINETESTING.TITLE', 'WORKSHEETS.HEADLINETESTING.SUMMARY', 'WORKSHEETS.HEADLINETESTING.DESCRIPTION',
              'WORKSHEETS.HEADLINETESTING.WORKSHEET_1.URL',
  
              'WORKSHEETS.FINDIT.TITLE', 'WORKSHEETS.FINDIT.SUMMARY', 'WORKSHEETS.FINDIT.DESCRIPTION',
              'WORKSHEETS.FINDIT.WORKSHEET_1.URL',
  
              'WORKSHEETS.JARGONBUSTER.TITLE', 'WORKSHEETS.JARGONBUSTER.SUMMARY', 'WORKSHEETS.JARGONBUSTER.DESCRIPTION',
              'WORKSHEETS.JARGONBUSTER.WORKSHEET_1.URL',
  
              'WORKSHEETS.TITANIC.TITLE', 'WORKSHEETS.TITANIC.SUMMARY', 'WORKSHEETS.TITANIC.DESCRIPTION',
              'WORKSHEETS.TITANIC.WORKSHEET_1.URL',
  
              'WORKSHEETS.ZOMBIEESCAPE.TITLE', 'WORKSHEETS.ZOMBIEESCAPE.SUMMARY', 'WORKSHEETS.ZOMBIEESCAPE.DESCRIPTION',
              'WORKSHEETS.ZOMBIEESCAPE.WORKSHEET_1.URL',
  
              'WORKSHEETS.PHISHING.TITLE', 'WORKSHEETS.PHISHING.SUMMARY', 'WORKSHEETS.PHISHING.DESCRIPTION',
              'WORKSHEETS.PHISHING.WORKSHEET_1.URL'
  
          ]).then(function (translations) {
              vm.worksheets = [
                  {
                      title : translations['WORKSHEETS.SMARTCLASSROOM.TITLE'],
                      summary : translations['WORKSHEETS.SMARTCLASSROOM.SUMMARY'],
                      description : translations['WORKSHEETS.SMARTCLASSROOM.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'text',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-smartclassroom.png',
                      tags : [ 'digital assistants', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-smartclassroom.pdf',
                      downloads : [
                          {
                              description : translations['WORKSHEETS.SMARTCLASSROOM.WORKSHEET_1.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.SMARTCLASSROOM.WORKSHEET_1.URL']
                          },
                          {
                              description : translations['WORKSHEETS.SMARTCLASSROOM.WORKSHEET_2.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.SMARTCLASSROOM.WORKSHEET_2.URL']
                          },
                          {
                              description : translations['WORKSHEETS.SMARTCLASSROOM.WORKSHEET_3.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.SMARTCLASSROOM.WORKSHEET_3.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.MAKEMEHAPPY.TITLE'],
                      summary : translations['WORKSHEETS.MAKEMEHAPPY.SUMMARY'],
                      description : translations['WORKSHEETS.MAKEMEHAPPY.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'text',
                      maketypes : [ 'scratch3', 'python' ],
                      image : 'static/images/project-makemehappy.png',
                      tags : [ 'sentiment analysis', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-makemehappy.pdf',
                      downloads : [
                          {
                              description : translations['WORKSHEETS.MAKEMEHAPPY.WORKSHEET_1.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.MAKEMEHAPPY.WORKSHEET_1.URL']
                          },
                          {
                              description : translations['WORKSHEETS.MAKEMEHAPPY.WORKSHEET_2.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.MAKEMEHAPPY.WORKSHEET_2.URL']
                          },
                          {
                              description : translations['WORKSHEETS.MAKEMEHAPPY.WORKSHEET_3.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.MAKEMEHAPPY.WORKSHEET_3.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.SNAP.TITLE'],
                      summary : translations['WORKSHEETS.SNAP.SUMMARY'],
                      description : translations['WORKSHEETS.SNAP.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'images',
                      maketypes : [ 'scratch2' ],
                      image : 'static/images/project-snap.png',
                      tags : [ 'image classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-snap.pdf',
                      downloads : [
                          {
                              description : translations['WORKSHEETS.SNAP.WORKSHEET_1.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.SNAP.WORKSHEET_1.URL']
                          },
                          {
                              description : translations['WORKSHEETS.SNAP.WORKSHEET_2.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.SNAP.WORKSHEET_2.URL'],
                              resources : translations['WORKSHEETS.SNAP.WORKSHEET_2.RESOURCES']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.CHAMELEON.TITLE'],
                      summary : translations['WORKSHEETS.CHAMELEON.SUMMARY'],
                      description : translations['WORKSHEETS.CHAMELEON.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-chameleon.png',
                      tags : [ 'image classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-chameleon.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.CHAMELEON.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.TITANIC.TITLE'],
                      summary : translations['WORKSHEETS.TITANIC.SUMMARY'],
                      description : translations['WORKSHEETS.TITANIC.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'numbers',
                      maketypes : [ 'python' ],
                      image : 'static/images/project-titanic.png',
                      tags : [ 'predictive model', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-titanic.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.TITANIC.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.MAILMANMAX.TITLE'],
                      summary : translations['WORKSHEETS.MAILMANMAX.SUMMARY'],
                      description : translations['WORKSHEETS.MAILMANMAX.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-mailmanmax.png',
                      tags : [ 'optical character recognition', 'handwriting recognition', 'image classification' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-mailmanmax.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.MAILMANMAX.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.CARORCUP.TITLE'],
                      summary : translations['WORKSHEETS.CARORCUP.SUMMARY'],
                      description : translations['WORKSHEETS.CARORCUP.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-carorcup.png',
                      tags : [ 'image classification', 'supervised learning', 'crowd sourcing' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-carorcup.pdf',
                      downloads : [
                          {
                              description : translations['WORKSHEETS.CARORCUP.WORKSHEET_1.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.CARORCUP.WORKSHEET_1.URL']
                          },
                          {
                              description : translations['WORKSHEETS.CARORCUP.WORKSHEET_2.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.CARORCUP.WORKSHEET_2.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.FACELOCK.TITLE'],
                      summary : translations['WORKSHEETS.FACELOCK.SUMMARY'],
                      description : translations['WORKSHEETS.FACELOCK.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'images',
                      maketypes : [ 'scratch2' ],
                      image : 'static/images/project-facelock.png',
                      tags : [ 'facial recognition', 'biometrics', 'image classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-facelock.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.FACELOCK.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.JOURNEYTOSCHOOL.TITLE'],
                      summary : translations['WORKSHEETS.JOURNEYTOSCHOOL.SUMMARY'],
                      description : translations['WORKSHEETS.JOURNEYTOSCHOOL.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'numbers',
                      maketypes : [ 'scratch2' ],
                      image : 'static/images/project-journeytoschool.png',
                      tags : [ 'predictive model', 'testing and accuracy', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-journeytoschool.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.JOURNEYTOSCHOOL.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.SHYPANDA.TITLE'],
                      summary : translations['WORKSHEETS.SHYPANDA.SUMMARY'],
                      description : translations['WORKSHEETS.SHYPANDA.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-shypanda.png',
                      tags : [ 'image classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-shypanda.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.SHYPANDA.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.ALIENLANGUAGE.TITLE'],
                      summary : translations['WORKSHEETS.ALIENLANGUAGE.SUMMARY'],
                      description : translations['WORKSHEETS.ALIENLANGUAGE.DESCRIPTION'],
                      difficulty : 'Beginner',
                      type : 'sounds',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-alienlanguage.png',
                      tags : [ 'sound recognition', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-alienlanguage.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.ALIENLANGUAGE.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.PACMAN.TITLE'],
                      summary : translations['WORKSHEETS.PACMAN.SUMMARY'],
                      description : translations['WORKSHEETS.PACMAN.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'numbers',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-pacman.png',
                      tags : [ 'decision tree learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-pacman.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.PACMAN.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.CHATBOTS.TITLE'],
                      summary : translations['WORKSHEETS.CHATBOTS.SUMMARY'],
                      description : translations['WORKSHEETS.CHATBOTS.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'text',
                      maketypes : [ 'scratch3', 'python' ],
                      image : 'static/images/project-chatbots.png',
                      tags : [ 'sentiment analysis', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-chatbots.pdf',
                      downloads : [
                          {
                              description : translations['WORKSHEETS.CHATBOTS.WORKSHEET_1.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.CHATBOTS.WORKSHEET_1.URL']
                          },
                          {
                              description : translations['WORKSHEETS.CHATBOTS.WORKSHEET_2.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.CHATBOTS.WORKSHEET_2.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.ZOMBIEESCAPE.TITLE'],
                      summary : translations['WORKSHEETS.ZOMBIEESCAPE.SUMMARY'],
                      description : translations['WORKSHEETS.ZOMBIEESCAPE.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'numbers',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-zombieescape.png',
                      tags : [ 'supervised learning', 'feature selection', 'decision tree learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-zombieescape.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.ZOMBIEESCAPE.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.VIRTUALPET.TITLE'],
                      summary : translations['WORKSHEETS.VIRTUALPET.SUMMARY'],
                      description : translations['WORKSHEETS.VIRTUALPET.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-virtualpet.png',
                      tags : [ 'image classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-virtualpet.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.VIRTUALPET.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.TOURISTINFO.TITLE'],
                      summary : translations['WORKSHEETS.TOURISTINFO.SUMMARY'],
                      description : translations['WORKSHEETS.TOURISTINFO.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'text',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-touristinfo.png',
                      tags : [ 'training bias', 'recommendations', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-touristinfo.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.TOURISTINFO.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.SECRETCODE.TITLE'],
                      summary : translations['WORKSHEETS.SECRETCODE.SUMMARY'],
                      description : translations['WORKSHEETS.SECRETCODE.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'sounds',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-secretcode.png',
                      tags : [ 'speech recognition', 'sound recognition', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-secretcode.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.SECRETCODE.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.SORTINGHAT.TITLE'],
                      summary : translations['WORKSHEETS.SORTINGHAT.SUMMARY'],
                      description : translations['WORKSHEETS.SORTINGHAT.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'text',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-sortinghat.png',
                      tags : [ 'text classification', 'supervised learning', 'crowd sourcing' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-sortinghat.pdf',
                      downloads : [
                          {
                              description : translations['WORKSHEETS.SORTINGHAT.WORKSHEET_1.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.SORTINGHAT.WORKSHEET_1.URL']
                          },
                          {
                              description : translations['WORKSHEETS.SORTINGHAT.WORKSHEET_2.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.SORTINGHAT.WORKSHEET_2.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.ROCKPAPERSCISSORS.TITLE'],
                      summary : translations['WORKSHEETS.ROCKPAPERSCISSORS.SUMMARY'],
                      description : translations['WORKSHEETS.ROCKPAPERSCISSORS.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-rockpaperscissors.png',
                      tags : [ 'image classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-rockpaperscissors.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.ROCKPAPERSCISSORS.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.NEWSPAPERSHELVES.TITLE'],
                      summary : translations['WORKSHEETS.NEWSPAPERSHELVES.SUMMARY'],
                      description : translations['WORKSHEETS.NEWSPAPERSHELVES.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'text',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-headlines-easy.png',
                      tags : [ 'text classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-headlines-easy.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.NEWSPAPERSHELVES.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.JUDGEABOOK.TITLE'],
                      summary : translations['WORKSHEETS.JUDGEABOOK.SUMMARY'],
                      description : translations['WORKSHEETS.JUDGEABOOK.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-judgeabook.png',
                      tags : [ 'image classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-judgeabook.pdf',
                      downloads : [
                          {
                              description : translations['WORKSHEETS.JUDGEABOOK.WORKSHEET_1.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.JUDGEABOOK.WORKSHEET_1.URL']
                          },
                          {
                              description : translations['WORKSHEETS.JUDGEABOOK.WORKSHEET_2.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.JUDGEABOOK.WORKSHEET_2.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.LOCATELARRY.TITLE'],
                      summary : translations['WORKSHEETS.LOCATELARRY.SUMMARY'],
                      description : translations['WORKSHEETS.LOCATELARRY.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-locatelarry.png',
                      tags : [ 'image classification', 'supervised learning', 'image pre-processing' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-locatelarry.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.LOCATELARRY.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.CONFUSED.TITLE'],
                      summary : translations['WORKSHEETS.CONFUSED.SUMMARY'],
                      description : translations['WORKSHEETS.CONFUSED.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'images',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-confused.png',
                      tags : [ 'image classification', 'supervised learning', 'overfitting' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-confused.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.CONFUSED.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.SCHOOLLIBRARY.TITLE'],
                      summary : translations['WORKSHEETS.SCHOOLLIBRARY.SUMMARY'],
                      description : translations['WORKSHEETS.SCHOOLLIBRARY.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'numbers',
                      maketypes : [ 'scratch2' ],
                      image : 'static/images/project-schoollibrary.png',
                      tags : [ 'predictive model', 'recommendations', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-schoollibrary.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.SCHOOLLIBRARY.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.JARGONBUSTER.TITLE'],
                      summary : translations['WORKSHEETS.JARGONBUSTER.SUMMARY'],
                      description : translations['WORKSHEETS.JARGONBUSTER.DESCRIPTION'],
                      difficulty : 'Intermediate',
                      type : 'sounds',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-jargonbuster.png',
                      tags : [ 'speech recognition', 'sound recognition', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-jargonbuster.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.JARGONBUSTER.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.WHATTWITTERTHINKS.TITLE'],
                      summary : translations['WORKSHEETS.WHATTWITTERTHINKS.SUMMARY'],
                      description : translations['WORKSHEETS.WHATTWITTERTHINKS.DESCRIPTION'],
                      difficulty : 'Advanced',
                      type : 'text',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-whattwitterthinks.png',
                      tags : [ 'sentiment analysis', 'social media analysis', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-whattwitterthinks.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.WHATTWITTERTHINKS.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.NOUGHTSANDCROSSES.TITLE'],
                      summary : translations['WORKSHEETS.NOUGHTSANDCROSSES.SUMMARY'],
                      description : translations['WORKSHEETS.NOUGHTSANDCROSSES.DESCRIPTION'],
                      difficulty : 'Advanced',
                      type : 'numbers',
                      maketypes : [ 'scratch2', 'python' ],
                      image : 'static/images/project-noughtsandcrosses.png',
                      tags : [ 'decision tree learning', 'reinforcement learning', 'categorical data' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-noughtsandcrosses.pdf',
                      downloads : [
                          {
                              description : translations['WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_1.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_1.URL']
                          },
                          {
                              description : translations['WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_2.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_2.URL']
                          },
                          {
                              description : translations['WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_3.DESCRIPTION'],
                              worksheet : translations['WORKSHEETS.NOUGHTSANDCROSSES.WORKSHEET_3.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.TOPTRUMPS.TITLE'],
                      summary : translations['WORKSHEETS.TOPTRUMPS.SUMMARY'],
                      description : translations['WORKSHEETS.TOPTRUMPS.DESCRIPTION'],
                      difficulty : 'Advanced',
                      type : 'numbers',
                      maketypes : [ 'scratch2' ],
                      image : 'static/images/project-toptrumps.png',
                      tags : [ 'decision tree learning', 'reinforcement learning', 'categorical data' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-toptrumps.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.TOPTRUMPS.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.HEADLINETESTING.TITLE'],
                      summary : translations['WORKSHEETS.HEADLINETESTING.SUMMARY'],
                      description : translations['WORKSHEETS.HEADLINETESTING.DESCRIPTION'],
                      difficulty : 'Advanced',
                      type : 'text',
                      maketypes : [ 'scratch3' ],
                      image : 'static/images/project-headlines.png',
                      tags : [ 'text classification', 'supervised learning', 'testing' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-headlines.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.HEADLINETESTING.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.FINDIT.TITLE'],
                      summary : translations['WORKSHEETS.FINDIT.SUMMARY'],
                      description : translations['WORKSHEETS.FINDIT.DESCRIPTION'],
                      difficulty : 'Advanced',
                      type : 'images',
                      maketypes : [ 'appinventor' ],
                      image : 'static/images/project-findit.png',
                      tags : [ 'image classification', 'supervised learning' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-findit.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.FINDIT.WORKSHEET_1.URL']
                          }
                      ]
                  },
                  {
                      title : translations['WORKSHEETS.PHISHING.TITLE'],
                      summary : translations['WORKSHEETS.PHISHING.SUMMARY'],
                      description : translations['WORKSHEETS.PHISHING.DESCRIPTION'],
                      difficulty : 'Advanced',
                      type : 'numbers',
                      maketypes : [ 'python' ],
                      image : 'static/images/dataset-phishing.png',
                      tags : [ 'decision tree learning', 'supervised learning', 'categorical data' ],
                      teachersnotes : 'https://github.com/IBM/taxinomitis-docs/raw/master/teachers-notes/pdf/teachersnotes-phishing.pdf',
                      downloads : [
                          {
                              worksheet : translations['WORKSHEETS.PHISHING.WORKSHEET_1.URL']
                          }
                      ]
                  }
              ];
          });
  
          vm.downloadWorksheet = function (ev, worksheet) {
              $mdDialog.show({
                  locals : {
                      worksheet : worksheet
                  },
                  controller : function ($scope, locals) {
                      $scope.worksheet = locals.worksheet;
                      $scope.hide = function() {
                          $mdDialog.hide();
                      };
                      $scope.cancel = function() {
                          $mdDialog.cancel();
                      };
                  },
                  templateUrl : 'static/components-' + $stateParams.VERSION + '/worksheets/download.tmpl.html',
                  targetEvent : ev,
                  clickOutsideToClose : true
              });
          };
  
  
          $scope.projecttype = 'ALL';
          $scope.projectdifficulty = 'ALL';
          $scope.projectmaketype = 'ALL';
  
          $scope.filterWorksheets = function (item) {
              var match = true;
  
              if (($scope.projecttype !== 'ALL') && match) {
                  match = (item.type === $scope.projecttype);
              }
              if (($scope.projectdifficulty !== 'ALL') && match) {
                  match = (item.difficulty === $scope.projectdifficulty);
              }
              if (($scope.projectmaketype !== 'ALL') && match) {
                  match = (($scope.projectmaketype === 'python' && item.maketypes.indexOf('python') >= 0) ||
                           ($scope.projectmaketype === 'appinventor' && item.maketypes.indexOf('appinventor') >= 0) ||
                           (($scope.projectmaketype === 'scratch') && (item.maketypes.indexOf('scratch2') >= 0 ||
                                                                       item.maketypes.indexOf('scratch3') >= 0)));
              }
              return match;
          };
  
      }
  }());
  