diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3ee22e5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..44f3970 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/client/ \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..a6e5297 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "loopback" +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 2125666..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore index c864196..aff1045 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,19 @@ -bower_components/ -build/ -node_modules/ -.idea/ +*.csv +*.dat +*.iml +*.log +*.out +*.pid +*.seed +*.sublime-* +*.swo +*.swp +*.tgz +*.xml +.DS_Store +.idea +.project +.strong-pm +coverage +node_modules +npm-debug.log diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..02f3fc1 --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,3 @@ +{ + "generator-loopback": {} +} \ No newline at end of file diff --git a/app.js b/app.js deleted file mode 100644 index a1f7af8..0000000 --- a/app.js +++ /dev/null @@ -1,40 +0,0 @@ -var express = require('express'); -var path = require('path'); -var favicon = require('serve-favicon'); -var logger = require('morgan'); -var cookieParser = require('cookie-parser'); -var bodyParser = require('body-parser'); - -var app = express(); - -// view engine setup -app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'jade'); - -// uncomment after placing your favicon in /public -//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); -app.use(logger('dev')); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: false })); -app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'frontend/build/unbundled'))); - -// catch 404 and forward to error handler -app.use(function(req, res, next) { - var err = new Error('Not Found'); - err.status = 404; - next(err); -}); - -// error handler -app.use(function(err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; - - // render the error page - res.status(err.status || 500); - res.render('error'); -}); - -module.exports = app; diff --git a/bin/www b/bin/www deleted file mode 100644 index 245c0f2..0000000 --- a/bin/www +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -var app = require('../app'); -var debug = require('debug')('pwd-mt:server'); -var http = require('http'); - -/** - * Get port from environment and store in Express. - */ - -var port = normalizePort(process.env.PORT || '8080'); -app.set('port', port); - -/** - * Create HTTP server. - */ - -var server = http.createServer(app); - -/** - * Listen on provided port, on all network interfaces. - */ - -server.listen(port); -server.on('error', onError); -server.on('listening', onListening); - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - var port = parseInt(val, 10); - - if (isNaN(port)) { - // named pipe - return val; - } - - if (port >= 0) { - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "error" event. - */ - -function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - var bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - console.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } -} - -/** - * Event listener for HTTP server "listening" event. - */ - -function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - debug('Listening on ' + bind); -} diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..dd00c9e --- /dev/null +++ b/client/README.md @@ -0,0 +1,3 @@ +## Client + +This is the place for your application front-end files. diff --git a/common/models/exercise.js b/common/models/exercise.js new file mode 100644 index 0000000..7e957d3 --- /dev/null +++ b/common/models/exercise.js @@ -0,0 +1,184 @@ +'use strict'; + +module.exports = function (Exercise) { + + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * */ + /* R E M O T E M E T H O D S */ + /* * * * * * * * * * * * * * * * * * * * * * * * * * */ + + + /** + * This Method adds the user as a participant to an exercise and + * Updates the exercise array in the user. + * @param exerciseId The Exercise a user is enrolled in + * @param cb Callback function + */ + + Exercise.enroll = function (exerciseId, options, cb) { + + var app = Exercise.app; + var userId = options.accessToken.userId; + Exercise.find({where: {id: exerciseId}}, function (err, ex) { + + if (err) { + console.log(err); + cb(new Error("Could not query Exercise Model", null)); + return; + } + + if (ex.length < 1) { + cb(new Error("No Exercise with this ID found", null)); + return; + } + var newParticipants = ex[0].participantsUserIds; + // Casting IDs to Strings so they are searchable + var participantsStrings = []; + for (var i = 0; i < newParticipants.length; i++) { + participantsStrings.push(newParticipants[i] + ""); + } + + if (participantsStrings.indexOf(userId + "") > -1) { // Safe Cast to String + cb(new Error("User is already part of this Exercise", null)); + return; + } + else { + newParticipants.push(userId); + Exercise.updateAll({id: exerciseId}, { + participantsUserIds: newParticipants + }, function (err, newEx) { + if (err) { + console.log(err); + cb(new Error("Could not update Exercise Model", null)); + return; + } + + // Update the User: + + app.models.PlatformUser.find({where: {id: userId}}, function(err, usersSearchResult) { + + if (err) throw err; + + if (usersSearchResult.length > 0) { + + var currentUser = usersSearchResult[0]; + var exerciseIds = currentUser.exerciseIds; + exerciseIds.push(exerciseId); + app.models.PlatformUser.updateAll({id: userId}, {exerciseIds: exerciseIds}, function (err, activeUserSearchResult) { + if (err) { + console.log(err); + cb(new Error("Could not update User Model", null)); + return; + } + }); + + } else { + + cb(new Error("No user was found when querying for current user id"), null); + + } + }); + + // SUCCESS CALLBACK + cb(null, newEx); + return; + } + ) + } + }); + }; + + Exercise.remoteMethod('enroll', { + accepts: [ + {arg: 'exerciseId', type: 'string'}, + {arg: "options", type: "object", http: "optionsFromRequest"} + ], + returns: {arg: 'exercise', type: 'object'} + }); + + + /** + * This Method deletes the user as a participant from an exercise and + * Updates the exercise array in the user. + * @param exerciseId The Exercise a user is deleted from + * @param cb Callback function + */ + + Exercise.disenroll = function (exerciseId, options, cb) { + + var app = Exercise.app; + var userId = options.accessToken.userId; + Exercise.find({where: {id: exerciseId}}, function (err, ex) { + + if (ex.length < 1) { + cb(new Error("No Exercise with this ID found", null)); + return; + } + + if (err) { + console.log(err); + cb(new Error("Could not query Exercise Model", null)); + return; + } + + if (ex.length < 0) { + cb(new Error("Invalid Exercise ID", null)); + return; + } + + var participants = ex[0].participantsUserIds; + var userIDString = userId + ""; + for (var i = 0; i < participants.length; i++) { + var partString = participants[i] + ""; + if (partString == userIDString) { + participants.splice(i, 1); + break; + } + } + + // Update Exercise Model + Exercise.updateAll({id: exerciseId}, { + participantsUserIds: participants + }, function (err, success) { + if (err) console.log(err); + + // Update PlaformUser Model: + app.models.PlatformUser.find({where: {id: userId}}, function (err, user) { + if (err) { + console.log(err); + cb(new Error("Could not update User Model", null)); + return; + } + var exercises = user[0].exerciseIds; + var exercisesStrings = []; + for (var i = 0; i < exercises.length; i++) { + exercisesStrings = exercises[i] + ""; + } + var deleteIndex = exercisesStrings.indexOf(exerciseId + ""); + exercises.splice(deleteIndex, 1); + app.models.PlatformUser.updateAll({id: userId}, {exerciseIds: exercises}, function (err, success) { + if (err) { + console.log(err); + cb(new Error(err, null)); + return; + } + + cb(null, ex); + + }) + }) + }); + }); + }; + + + Exercise.remoteMethod('disenroll', { + accepts: [ + {arg: 'exerciseId', type: 'string'}, + {arg: "options", type: "object", http: "optionsFromRequest"} + ], + returns: {arg: 'exercise', type: 'object'} + }); + +}; diff --git a/common/models/exercise.json b/common/models/exercise.json new file mode 100644 index 0000000..a4a4a64 --- /dev/null +++ b/common/models/exercise.json @@ -0,0 +1,79 @@ +{ + "name": "Exercise", + "base": "PersistedModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "name": { + "type": "string", + "required": true + }, + "date": { + "type": "date", + "required": true + }, + "tutor": { + "type": "string", + "required": false + }, + "location": { + "type": "string", + "required": true + } + }, + "validations": [], + "relations": { + "participants": { + "type": "referencesMany", + "model": "PlatformUser", + "foreignKey": "participantsUserIds" + }, + "semester": { + "type": "belongsTo", + "model": "Semester", + "foreignKey": "semesterId" + } + }, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW" + }, + { + "accessType": "WRITE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW" + }, + { + "accessType": "WRITE", + "principalType": "ROLE", + "principalId": "tutor", + "permission": "ALLOW" + }, + { + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW", + "property": "enroll" + }, + { + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW", + "property": "disenroll" + } + ], + "methods": {} +} diff --git a/common/models/group.js b/common/models/group.js new file mode 100644 index 0000000..bc5e418 --- /dev/null +++ b/common/models/group.js @@ -0,0 +1,482 @@ +'use strict'; + +module.exports = function (Group) { + + /* * * * * * * * * * * * * * * * * * * * * * * * * * */ + /* O P E R A T I O N H O O K S */ + /* * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /** This operation hook checks if the requesting user is a member + * of the group he wants to change or an admin. If neither of both, + * the operation is rejected. + */ + Group.observe('before save', function (context, next) { + + var currentInstance = null; + if (context.currentInstance) { + currentInstance = context.currentInstance; + } + if (context.instance) { + currentInstance = context.instance; + } + + var app = Group.app; + + var token = context.options && context.options.accessToken; + + // Context is null when model is accessed via node. We only + // want to restrict access for users of the REST API. + // + // For some reason, context is not null when accessing model + // from remote method in the same model. The key of the field + // containing the current instance gives us a clue whether this + // request comes via REST or via node. + if (token == undefined || context.currentInstance) { + // We're accessing via node + next(); + } else { + // Access via REST + // Get current user and group + + var userId = token.userId; + var contextGroup = currentInstance; + // If a new instance is saved the currentInstance is used in checkMembers + if (context.isNewInstance) { + checkMembers(app, currentInstance, next, userId); + } + + // If an instance is updated, the instance from the DB is used to allow for permission checking when + // a user is leaving a group + else { + Group.find({ + where: {id: contextGroup.id} + }, function (err, groupDB) { + + if(err) { + console.log(err); + } + else{ + checkMembers(app, groupDB[0], next, userId); + } + }); + } + } + }); + + + function checkMembers(app, group, next, userId) { + + // Get member IDs of current group + var groupMemberIdsAsStings = []; + for (var i = 0; i < group.groupMemberIds.length; i++) { + groupMemberIdsAsStings.push(group.groupMemberIds[i].id); + } + + // Get role ID of role 'admin' + app.models.Role.find({where: {name: 'admin'}}, function (err, role) { + + if (err) throw err; + + var adminRoleId = role[0].id; + + // Get all role mappings from current user to role 'admin' + app.models.RoleMapping.find({ + where: { + principalId: userId, + roleId: adminRoleId + } + }, + function (err, rolemappings) { + + if (err) throw err; + + // If user is neither a member of current group nor an admin, + // reject the operation with an error + if (groupMemberIdsAsStings.indexOf(userId.id) < 0 + && rolemappings.length === 0) { + var err = new Error("Berechtigung erforderlich"); + err.statusCode = 403; + next(err); + return; + } + + // Otherwise continue execution + next(); + }); + }); + } + + /** This operation hook makes sure all references to a group in + * other model instances are removed before the group is deleted. + */ + Group.observe('before delete', function(context, next){ + + var models = Group.app.models; + + Group.find({where: context.where}, function(err, affectedGroups){ + + if (err) throw err; + + // iterate over all affected Groups + affectedGroups.forEach(function(group){ + + // remove group reference in all members + var membersQueryById = group.groupMemberIds.map(function(value){return {id: value};}); + models.PlatformUser.updateAll( + {or: membersQueryById}, + {groupId: null}, + function(err, info){ + if (err) throw err; + }); + + // remove group reference in all labs + if (group.labIds.length > 0) { + var labQueryById = group.labIds.map(function(value){return {id: value};}); + models.Lab.updateAll( + {or: labQueryById}, + {groupId: null}, + function(err, info) { + if (err) throw err; + }); + } + + // remove all priority mappings of deleted group + models.Priority.destroyAll( + {groupId: group._id}, + function(err, info){ + if (err) throw err; + }); + + }); + + // actually remove the groups + next(); + + }); + }); + + function generateDefaultName (listOfGroups) { + + var existingNames = listOfGroups.map(function(object){return object.name}); + var numGroups = listOfGroups.length; + + var defaultNamePattern = /^Gruppe \d+$/; + + var highestExistingNumber = 0; + existingNames.forEach(function(name){ + if (defaultNamePattern.test(name)) { + var numberStringInName = name.split(' ')[1]; + var numberInName = parseInt(numberStringInName); + if (numberInName > highestExistingNumber) { + highestExistingNumber = numberInName; + } + } + }); + + if (highestExistingNumber > numGroups) { + return 'Gruppe ' + (highestExistingNumber + 1); + } else { + return 'Gruppe ' + (numGroups + 1); + } + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * */ + /* R E M O T E M E T H O D S */ + /* * * * * * * * * * * * * * * * * * * * * * * * * * */ + + Group.createByMail = function (emails, name, options, callback) { + + if (!emails) { + callback(new Error('Missing email attribute'), null); + return; + } + + var app = Group.app; + var userId = options.accessToken.userId; + + // Fetch active user + app.models.PlatformUser.find({ + where: { id: userId } + }, function(err, activeUserSearchResult){ + + if (err) throw err; + + var activeUser = activeUserSearchResult[0]; + + // Check if active user is already enrolled for a group + if (activeUser.groupId) { + Group.find({where: {id: activeUser.groupId}}, + function(err, groupSearchResult){ + + if (err) throw err; + callback(new Error('Active user is already enrolled for group ' + + activeUser.groupId + + ' (' + groupSearchResult[0].name + + ')'), null); + return; + + }); + } + + // Validate emails argument length + app.models.Semester.find( + {where: { id: activeUser.semesterId }}, + function(err, semesterSearchResult){ + + if (err) throw err; + + var activeSemester = semesterSearchResult[0]; + if (emails.length !== activeSemester.group_size - 1) { + callback(new Error('Invalid number of members. Group size for active semester is ' + + activeSemester.group_size), null); + return; + } + + // Validate emails argument data type + // and create db query array on the fly + var i; + var usersDBQuery = []; + for (i in emails) { + if (typeof(emails[i]) !== 'string') { + callback(new Error('Value of emails argument must be an array of strings'), null); + return; + } + usersDBQuery.push({email: emails[i]}); + } + + // Fetch user objects for specified email addresses + app.models.PlatformUser.find( + {where: { or: usersDBQuery}}, + function(err, usersSearchResult){ + + if (err) throw err; + + if (usersSearchResult.length !== emails.length) { + callback(new Error('Some of the specified email addresses do not match registered users'), null); + return; + } + + // Check specified users for: + // - correct role + // - correct semester + // - active group membership + // - not equal to active user + var j; + for (j=0; j|*|{x}|{}} + */ +function intersection (a, b) { + var temp = []; + return a.filter(function(val) { + if (b.indexOf(val) >= 0 && temp.indexOf(val) < 0) { + temp.push(val); + return true; + } else { + return false; + } + }); +} + +/** + * Returns the difference a \ b of two arrays + * + * @param a - an Array + * @param b - an Array + * @returns {Array} + */ +function difference (a, b) { + return a.filter(function(val){ + return b.indexOf(val) < 0; + }); +} + +module.exports = function(Labtype) { + + /** + * For a set of Lab instances with a given labTypeId, assign one tutor to each + * Lab - if possible - according to the preferences stored in the tutor's + * tutorPossibleLabs relation. Labs with no tutor candidate will have a + * tutorId of null. + * + * @param labTypeId + * @param next - middleware function + * @returns all affected Lab instances with changes made + */ + Labtype.autoDistributeTutors = function (labTypeId, next) { + + var Lab = Labtype.app.models.Lab, + PlatformUser = Labtype.app.models.PlatformUser, + + labTypeInstance, // labType for distribution + labsForDistribution, // all labs with this labType + labIds, // array of labsForDistribution.id values + tutorsForDistribution, // all tutors affected by this distribution + labMeta, // map with temporary information about affected labs (see below) + tutorMeta, // map with some statistical info about affected tutors (see below) + idealNumberOfLabs; // number of labs per tutor so that all tutors work approximately the same + + if (!labTypeId) { + next(new Error('No labTypeId specified'), null); + return; + } + + // Get specified labType instance + Labtype.find({where: {id: labTypeId}}, function (err, labTypeSearchResult) { + + if (err) throw err; + if (labTypeSearchResult.length == 0) { + next(new Error('LabType with id '+labTypeId+' not found'), null); + return; + } + + labTypeInstance = labTypeSearchResult[0]; + + // Get all Labs for this LabType + Lab.find({where: {labTypeId: labTypeId}}, function (err, labSearchResult) { + + if (err) throw err; + if (labSearchResult.length == 0) { + next(new Error('LabType with id '+labTypeId+' has no associated Labs'), null); + return; + } + + labsForDistribution = labSearchResult; + labIds = labsForDistribution.map(function(lab){ + return lab.id.toString(); + }); + + // Get all tutors that have marked at least one of labsForDistribution as possibleLab + PlatformUser.find( + { where: { tutorPossibleLabs: { inq: labIds } } }, + function (err, tutorSearchResult) { + + if (err) throw err; + if (tutorSearchResult.length == 0) { + next(new Error('No tutors have marked labs from this labType as possible'), null); + return; + } + + tutorsForDistribution = tutorSearchResult; + + console.log('\nStarting auto distribution of tutors for labTypeId '+labTypeId+'...'); + + idealNumberOfLabs = Math.floor(labsForDistribution.length / tutorsForDistribution.length); + + // Assemble labMeta map like this + // { + // <> => { + // assigned: false, + // candidates: [ <>, <>, ... ] + // }, + // ... + // } + labMeta = new Map(); + tutorsForDistribution.forEach(function(tutor){ + var possibleLabIds = tutor.tutorPossibleLabs(); + possibleLabIds.forEach(function(value){ + var possibleLabId = value.toString(); + if (labIds.indexOf(possibleLabId) >= 0) { + var existingVal = labMeta.get(possibleLabId); + if (existingVal) { + existingVal.candidates.push(tutor.id.toString()); + labMeta.set(possibleLabId, existingVal); + } else { + labMeta.set(possibleLabId, { + assigned: false, + candidates: [tutor.id.toString()] + }); + } + } + }); + }); + + // Assemble tutorMeta map like this + // { + // <> => { + // probabilities: [ 0.5, 0.66, 0.33, ... ], + // averageProbability: 0.5, + // invertedAverage: 0.3437, + // compensationFactor: 6, + // labsAssigned: 0 + // }, + // ... + // } + + // Store a probability value for each lab the tutor marked as possible + tutorMeta = new Map(); + labMeta.forEach(function(value){ + var probability = 1 / value.candidates.length; + value.candidates.forEach(function(tutorId){ + var existingValue = tutorMeta.get(tutorId); + if (existingValue && existingValue.probabilities) { + existingValue.probabilities.push(probability); + tutorMeta.set(tutorId, existingValue); + } else if (existingValue) { + existingValue.probabilities = [probability]; + tutorMeta.set(tutorId, existingValue); + } else { + tutorMeta.set(tutorId, { + probabilities: [probability], + labsAssigned: 0 + }); + } + }); + }); + + // Calculate the average probability of getting chosen for each tutor + tutorMeta.forEach(function(value, key){ + var probabilitiesSum = 0; + value.probabilities.forEach(function(probability){ + probabilitiesSum += probability; + }); + value.averageProbability = probabilitiesSum / value.probabilities.length; + tutorMeta.set(key, value); + }); + + // Calculate the invertedAverage for each tutor by mirroring its + // averageProbability along the average of all tutor's averageProbabilities. + // + // Then calculate the factor that will raise (or lower) its probability of being + // chosen to match this invertedAverage + var tutorMetaValues = Array.from(tutorMeta.values()); + var averages = tutorMetaValues.map(function(val){ + return val.averageProbability; + }); + var averageSum = averages.reduce(function(acc, val){ return acc + val; }, 0); + var averageOfAverages = averageSum / averages.length; + tutorMeta.forEach(function(value, key){ + var diff = averageOfAverages - value.averageProbability; + value.invertedAverage = averageOfAverages + diff; + value.compensationFactor = + Math.round((value.invertedAverage / value.averageProbability) * 10); + tutorMeta.set(key, value); + }); + + // Before making any changes to the model, reset all relations between + // affected Lab and PlatformUser instances + Lab.updateAll( + { labTypeId: labTypeId }, + { tutorId: null }, + function(err){ + + if (err) { + next(err, null); return; + } + + console.log('All affected models have been reset'); + + /** + * Private helper for assigning tutors to a lab and keeping track of + * all successful assignments in the meta maps labMeta and tutorMeta + * + * @param tutorId + * @param labId + */ + var assignTutorToLab = function (tutorId, labId) { + + return new Promise(function(resolve, reject){ + + Lab.find( + { where: { id: labId } }, + function (err, docs) { + + if (err) { + return reject(err); + } + if (docs.length == 0) { + return reject(new Error('No Lab with id '+labId+' found')); + } + + var lab = docs[0]; + if (lab.tutorId) { + return reject(new Error('Lab with id '+labId+' is already assigned to tutor '+lab.tutorId)); + } + + lab.updateAttribute('tutorId', tutorId, function (err, instance) { + + if (err) return reject(err); + + var labMetaEntry = labMeta.get(labId); + labMetaEntry.assigned = true; + labMeta.set(labId, labMetaEntry); + + var tutorMetaEntry = tutorMeta.get(tutorId); + tutorMetaEntry.labsAssigned += 1; + tutorMeta.set(tutorId, tutorMetaEntry); + + console.log('Updated Lab instance '+instance.id+': tutorId = '+tutorId); + + resolve(instance); + }); + }); + }); + }; + + // Assign all labs with only one candidate first + var singleCandidatePromises = []; + labsForDistribution.forEach(function(lab){ + + var labMetaEntry = labMeta.get(lab.id.toString()); + var labCandidates = (labMetaEntry) ? labMetaEntry.candidates : null; + if (labCandidates && labCandidates.length === 1) { + singleCandidatePromises.push(assignTutorToLab(labCandidates[0], lab.id.toString())); + } + }); + var singleCandidateWrapperPromise = Promise.all(singleCandidatePromises); + singleCandidateWrapperPromise.then(function(){ + + /** + * Private helper for choosing one of multiple possible tutors for a + * given Lab instance. + * + * @param labId - id of a Lab instance + */ + var assignTutorFromCandidates = function (labId) { + + var labMetaEntry = labMeta.get(labId); + if (labMetaEntry && !labMetaEntry.assigned) { + + var candidateIds = labMetaEntry.candidates; + + // Determine + // - a list of candidates for this lab that have less than the ideal + // number of labs assigned + // - the candidate with the least number of labs assigned + var candidatesWithCapacity = []; + var rarestAppointedCandidateId = candidateIds[0]; + candidateIds.forEach(function(candidateId){ + + var candidateMeta = tutorMeta.get(candidateId); + if (candidateMeta) { + + if (candidateMeta.labsAssigned < idealNumberOfLabs) { + candidatesWithCapacity.push(candidateId); } + if (candidateMeta.labsAssigned < tutorMeta.get(rarestAppointedCandidateId).labsAssigned) { + rarestAppointedCandidateId = candidateId; } + + } else { + throw new Error('No meta info for candidate '+candidateId+' found'); + } + }); + + // Choose a tutor according to these rules: + // - if there is no candidate with capacity, choose the least appointed candidate + // - otherwise create a set that contains each candidate as many times as defined + // by his compensationFactor and choose randomly from this set + var chosenTutorId; + if (candidatesWithCapacity.length === 0) { + chosenTutorId = rarestAppointedCandidateId; + } else { + + var compensatedSetOfCandidates = []; + candidatesWithCapacity.forEach(function(candidateId){ + + for (var i=0; i> => { + // assigned: false, + // candidates: Map { + // 0 => [ <>, <>, ... ], + // ... + // }, + // hasTutor: true + // } + // } + labsForDistribution.forEach(function(lab){ + labMeta.set(lab.id.toString(), { + assigned: false, + candidates: new Map(), + hasTutor: (lab.tutorId) ? true : false + }); + }); + + priorityMappings.forEach(function(priority){ + var labId = priority.labId.toString(), + groupId = priority.groupId.toString(), + priorityValue = priority.priority, + labMetaEntry = labMeta.get(labId); + + if (labMetaEntry.candidates.get(priorityValue)) { + var candidatesForValue = labMetaEntry.candidates.get(priorityValue); + candidatesForValue.push(groupId); + labMetaEntry.candidates.set(priorityValue, candidatesForValue); + } else { + labMetaEntry.candidates.set(priorityValue, [groupId]); + } + + labMeta.set(labId, labMetaEntry); + }); + + // Assemble groupMeta map like this + // + // groupMeta = Map { + // <> => { + // assigned: false + // } + // } + groupsForDistribution.forEach(function(group){ + groupMeta.set(group.id.toString(), { + assigned: false + }); + }); + + // Reset all affected Group and Lab models first + var labIds = labsForDistribution.map(function(lab){ + return lab.id.toString(); + }); + Lab.updateAll( + {labTypeId: labTypeId}, {groupId: null}, + function(err){ + + if (err) throw err; + groupsForDistribution.forEach(function(group, index){ + var labsInGroup = group.labIds.map(function(id){ return id.toString(); }); + var affectedLabsInGroup = intersection(labsInGroup, labIds); + group.updateAttribute('labIds', + difference(labsInGroup, affectedLabsInGroup), + function(err){ + if (err) throw err; + // We're being asynch again + }); + }); + + console.log('All affected values have been reset'); + + // Execute distribution algorithm + + // Determine lowest given priority value (highest integer) + var lowestPrio = 0; + labMeta.forEach(function(labMetaEntry){ + labMetaEntry.candidates.forEach(function(val, key){ + if (key > lowestPrio) { + lowestPrio = key; + } + }); + }); + + /** + * Selects and returns a random element of a given array + * + * @param arrayOfElements + * @returns {*} + */ + var selectRandom = function (arrayOfElements) { + if (arrayOfElements.length === 0) return null; + return arrayOfElements[Math.floor(Math.random()*arrayOfElements.length)]; + }; + + /** + * Private helper for bidirectionally assigning a group to a lab + * + * @param groupId + * @param labId + * @returns {Promise} - resolved when all operations completed successfully + */ + var assignGroupToLab = function (groupId, labId) { + + var lab, group, + result = { + group: {}, + lab: {} + }; + + return new Promise(function(resolve, reject){ + + Lab.find({where: {id: labId}}, function (err, labDocs){ + + if (err) return reject(err); + if (labDocs.length === 0) return reject(new Error('No lab with id '+labId+' found')); + lab = labDocs[0]; + + Group.find({where: {id: groupId}}, function (err, groupDocs) { + + if (err) return reject(err); + if (groupDocs.length === 0) return reject(new Error('No group with id '+groupId+' found')); + group = groupDocs[0]; + + lab.updateAttribute('groupId', group.id, function(err, instance){ + + if (err) return reject(err); + result.lab = instance; + + group.labs(null, function(err, labsOfGroup){ + + if (err) return reject(err); + var labIdsOfGroup = labsOfGroup.map(function(labObject){ + return labObject.id; + }); + labIdsOfGroup.push(lab.id); + group.updateAttribute('labIds', labIdsOfGroup, function(err, instance){ + if (err) return reject(err); + result.group = instance; + resolve(result); + }) + }); + }); + }) + }); + }); + }; + + groupsForDistribution.forEach(function(group){ + groupsForDistributionMap.set(group.id.toString(), group); + }); + + var prioIterator = 0; + var promises = []; + while (prioIterator <= lowestPrio) { + + labsForDistribution.forEach(function(lab){ + + var labMetaEntry = labMeta.get(lab.id.toString()); + if (labMetaEntry.hasTutor && !labMetaEntry.assigned) { + + // assemble a list of candidates for current prio that have + // not been assigned to a lab yet + var candidateIds = labMetaEntry.candidates.get(prioIterator); + var unassignedCandidateIds = []; + if (candidateIds && candidateIds.length > 0) { + candidateIds.forEach(function(candidateId){ + if (!groupMeta.get(candidateId).assigned) { + unassignedCandidateIds.push(candidateId); + } + }); + } + + // Choose a random group from this list of candidates + var chosenCandidateId; + if (unassignedCandidateIds.length > 0) { + chosenCandidateId = selectRandom(unassignedCandidateIds); + + console.log('Assigning Group '+chosenCandidateId+' to Lab '+lab.id); + + // Update meta maps + labMetaEntry.assigned = true; + labMeta.set(lab.id.toString(), labMetaEntry); + var groupMetaEntry = groupMeta.get(chosenCandidateId); + groupMetaEntry.assigned = true; + groupMeta.set(chosenCandidateId, groupMetaEntry); + + // Store choice in lab and group + promises.push( + assignGroupToLab(chosenCandidateId.toString(), lab.id.toString()) + ); + } + } + }); + prioIterator++; + } + + // If possible, assign all remaining groups to a lab they didn't choose + var remainingLabs = labsForDistribution.filter(function(lab){ + return !labMeta.get(lab.id.toString()).assigned; + }); + var remainingGroups = groupsForDistribution.filter(function(group){ + return !groupMeta.get(group.id.toString()).assigned; + }); + + while (remainingLabs.length > 0 && remainingGroups.length > 0) { + var labHead = remainingLabs.shift(), + groupHead = remainingGroups[0], + labMetaEntry = labMeta.get(labHead.id.toString()); + + if (labMetaEntry.hasTutor) { + console.log('Assigning Group '+groupHead.id+' to Lab '+labHead.id+ + ' even though this was not on their priority list'); + promises.push( + assignGroupToLab(groupHead.id.toString(), labHead.id.toString()) + ); + remainingGroups.shift(); + } + } + + // wait for completion of all database operations + var wrapperPromise = Promise.all(promises); + wrapperPromise.then( + + // success + function(){ + + Lab.find({where: {labTypeId: labTypeId}}, function(err, labsForResponse){ + + if (err) throw err; + if (labsForResponse.length == 0) { + next(new Error('LabType with id ' + labTypeId + ' has no associated Labs'), null); + return; + } + + next(null, labsForResponse); + return; + }); + }, + + // error + function(reason){ + console.log(reason); + next(reason, null); + return; + }); + }); + }); + }); + }); + }); + }; + + Labtype.remoteMethod('autoDistributeGroups',{ + accepts: [ + {arg: 'labTypeId', type: 'string'} + ], + returns: [ + {type: 'object', root: true} + ] + }); + +}; diff --git a/common/models/lab-type.json b/common/models/lab-type.json new file mode 100644 index 0000000..93b70ba --- /dev/null +++ b/common/models/lab-type.json @@ -0,0 +1,80 @@ +{ + "name": "LabType", + "base": "PersistedModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "type": { + "type": "number", + "required": true + }, + "type_str": { + "type": "string", + "required": true + }, + "registration_deadline": { + "type": "date", + "required": true + }, + "registration_open": { + "type": "boolean", + "required": true, + "default": false + }, + "registration_deadline_tutors": { + "type": "date" + }, + "description_tutor": { + "type": "string" + }, + "description_student": { + "type": "string" + } + }, + "validations": [], + "relations": { + "semester": { + "type": "belongsTo", + "model": "Semester", + "foreignKey": "semesterId" + } + }, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW" + }, + { + "accessType": "WRITE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW", + "property": "autoDistributeTutors" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW", + "property": "autoDistributeGroups" + } + ], + "methods": {} +} diff --git a/common/models/lab.js b/common/models/lab.js new file mode 100644 index 0000000..04e1193 --- /dev/null +++ b/common/models/lab.js @@ -0,0 +1,113 @@ +'use strict'; + +module.exports = function(Lab) { + + /** + * Sets the `passed` flag of a given lab instance and updates all + * members of the assigned group accordingly. + * + * @param labId - ID of the lab to change + * @param passed - new value of the `passed` flag + * @param next - callback + * @returns the updated lab instance + */ + Lab.setPassed = function (labId, passed, next) { + + var Group = Lab.app.models.Group, + PlatformUser = Lab.app.models.PlatformUser, + affectedLab, affectedGroup, affectedUsers, + passedLabsPerUser = new Map(); + + // Get lab + Lab.find({ where: {id: labId} }, function(err, labSearchResult){ + + if (err) throw err; + if (labSearchResult.length === 0) { + next(new Error('No lab with id '+labId+' found'), null); + return; + } + + affectedLab = labSearchResult[0]; + + // Has group? + if (!affectedLab.groupId) { + next(new Error('Lab with id '+labId+' has no group assigned'), null); + return; + } + + // Get group + Group.find({ where: {id: affectedLab.groupId} }, function(err, groupSearchResult) { + + if (err) throw err; + if (groupSearchResult.length === 0) { + next(new Error('No group with id '+affectedLab.groupId+' found'), null); + return; + } + + affectedGroup = groupSearchResult[0]; + + // Get group members + PlatformUser.find({where: {id: {inq: affectedGroup.groupMemberIds}}}, function(err, userSearchResult){ + + if (err) throw err; + if (userSearchResult.length === 0) { + next(new Error('No users with specified IDs found'), null); + return; + } + + // Create a map of passedLabTypes per memberId + affectedUsers = userSearchResult; + var userSaveOperations = []; + affectedUsers.forEach(function(user) { + var passedLabTypes = user.passedLabTypesIds; + var passedLabTypesAsStrings = passedLabTypes.map(function(id){ + return id.toString(); + }); + var searchIndex = passedLabTypesAsStrings.indexOf(affectedLab.labTypeId.toString()); + if (passed && searchIndex < 0) { + // User has passed the lab but is not yet stored in his passedLabs + passedLabTypes.push(affectedLab.labTypeId); + } else if (!passed && searchIndex >= 0) { + // User has not passed the lab but it is still stored in his passedLabs + passedLabTypes.splice(searchIndex, 1); + } + // Update user asynchronously + userSaveOperations.push( + new Promise(function(resolve, reject){ + user.updateAttribute('passedLabTypesIds', passedLabTypes, function(err){ + if (err) { + throw err; + return reject(err); + } + resolve(); + }); + })); + }); + + var userWrapperPromise = Promise.all(userSaveOperations); + userWrapperPromise.then(function(){ + + // Update Lab + affectedLab.updateAttribute('passed', passed, function(err, updatedLabInstance){ + + if (err) throw err; + next(null, updatedLabInstance); + + }); + }); + }); + }); + }); + }; + + Lab.remoteMethod('setPassed', { + accepts: [ + {arg: 'labId', type: 'string'}, + {arg: 'passed', type: 'boolean'} + ], + returns: [ + {type: 'object', root: true} + ] + }); + +}; diff --git a/common/models/lab.json b/common/models/lab.json new file mode 100644 index 0000000..9149f9d --- /dev/null +++ b/common/models/lab.json @@ -0,0 +1,91 @@ +{ + "name": "Lab", + "base": "PersistedModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "date": { + "type": "date", + "required": true + }, + "duration": { + "type": "number" + }, + "location": { + "type": "string", + "required": true + }, + "passed": { + "type": "boolean", + "required": true, + "default": false + } + }, + "validations": [], + "relations": { + "labType": { + "type": "belongsTo", + "model": "LabType", + "foreignKey": "labTypeId" + }, + "semester": { + "type": "belongsTo", + "model": "Semester", + "foreignKey": "semesterId" + }, + "group": { + "type": "belongsTo", + "model": "Group", + "foreignKey": "groupId" + }, + "tutor": { + "type": "belongsTo", + "model": "PlatformUser", + "foreignKey": "tutorId" + } + }, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW" + }, + { + "accessType": "WRITE", + "principalType": "ROLE", + "principalId": "tutor", + "permission": "ALLOW" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "tutor", + "permission": "ALLOW", + "property": "setPassed" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW", + "property": "setPassed" + }, + { + "accessType": "WRITE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW" + } + ], + "methods": {} +} diff --git a/common/models/news-entry.js b/common/models/news-entry.js new file mode 100644 index 0000000..87f866c --- /dev/null +++ b/common/models/news-entry.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function(NewsEntry) { + +}; diff --git a/common/models/news-entry.json b/common/models/news-entry.json new file mode 100644 index 0000000..1741e3f --- /dev/null +++ b/common/models/news-entry.json @@ -0,0 +1,44 @@ +{ + "name": "NewsEntry", + "plural": "NewsEntries", + "base": "PersistedModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "message": { + "type": "string" + } + }, + "validations": [], + "relations": {}, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW" + }, + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "tutor", + "permission": "ALLOW" + }, + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW" + } + ], + "methods": {} +} diff --git a/common/models/pending-platform-user.js b/common/models/pending-platform-user.js new file mode 100644 index 0000000..84a6ca9 --- /dev/null +++ b/common/models/pending-platform-user.js @@ -0,0 +1,64 @@ +'use strict'; + +module.exports = function(Pendingplatformuser) { + + /** Operation hook for validation of new PendingPlatformUsers. + */ + Pendingplatformuser.observe('before save', function (context, next) { + + var PlatformUser = Pendingplatformuser.app.models.PlatformUser; + + function resumeCreation () { + + // Make sure whitelist entry has not more than one special role + // - isAdmin overrules isTutor + // - isAdmin == false && isTutor == false implies that the user is a student + if (context.instance.isAdmin) { + context.instance.isTutor = false; + } + + next(); + } + + if (context.isNewInstance) { + + // Make sure email address is not already listed on whitelist + Pendingplatformuser.find({}, function(err, whitelistSearchResult) { + + if (err) throw err; + + var pendingMails = whitelistSearchResult.map(function(obj){return obj.email}); + var pendingSearchIndex = pendingMails.indexOf(context.instance.email); + if (pendingSearchIndex >= 0) { + Pendingplatformuser.destroyById(whitelistSearchResult[pendingSearchIndex].id, function(err){ + if (err) throw err; + resumeCreation(); + }); + return; + } + + // Make sure email address is not already registered + PlatformUser.find({}, function(err, userSearchResult) { + + if (err) throw err; + + var registeredMails = userSearchResult.map(function(obj){return obj.email}); + var registeredSearchIndex = registeredMails.indexOf(context.instance.email); + if (registeredSearchIndex >= 0) { + PlatformUser.destroyById(userSearchResult[registeredSearchIndex].id, function(err){ + if (err) throw err; + resumeCreation(); + }); + } else { + resumeCreation(); + } + }); + }); + + } else { + next(); + } + + }); + +}; diff --git a/common/models/pending-platform-user.json b/common/models/pending-platform-user.json new file mode 100644 index 0000000..a7b4853 --- /dev/null +++ b/common/models/pending-platform-user.json @@ -0,0 +1,69 @@ +{ + "name": "PendingPlatformUser", + "base": "PersistedModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "name": { + "type": "string", + "required": true + }, + "first_name": { + "type": "string", + "required": true + }, + "email": { + "type": "string", + "required": true + }, + "isTutor": { + "type": "boolean", + "required": true, + "default": false + }, + "isAdmin": { + "type": "boolean", + "required": true, + "default": false + } + }, + "validations": [], + "relations": { + "semester": { + "type": "belongsTo", + "model": "Semester", + "foreignKey": "semesterId", + "required": true + } + }, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "DENY" + }, + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "tutor", + "permission": "ALLOW" + }, + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW" + } + ], + "methods": {} +} diff --git a/common/models/platform-user.js b/common/models/platform-user.js new file mode 100644 index 0000000..14e3569 --- /dev/null +++ b/common/models/platform-user.js @@ -0,0 +1,568 @@ +'use strict'; +var app = require('../../server/server'); + +module.exports = function (Platformuser) { + + /** Define an observer that fires before every save operation on the + * PlatformUsers (User Registration) table. If the E-Mail adress was not + * added to the PendingPlatform table by an admin or tutor ("whitelisted"), + * the registration will fail by throwing an 403 error. + * + * After that, it is checked if the user is whitelisted as a tutor + * (isTutor = true) and the field is set accordingly. + */ + Platformuser.observe('before save', function (context, next) { + + if (context.instance && context.isNewInstance) { + + var user = context.instance; + var PendingPlatformUser = app.models.PendingPlatformUser; + + PendingPlatformUser.find({where: {email: user.email}}, function (err, res) { + + if (res.length < 1) { + + var err = new Error("User can not be registered. Email has to be whitelisted by administrator"); + err.statusCode = 403; + next(err); + return; + + } else { + + // Check if the new user is whitelisted with a special role. + // + // The following rules apply: + // - isAdmin overrules isTutor + // - isAdmin == false && isTutor == false implies isStudent == true + // + // Attributes isTutor and isAdmin in the request object are ignored. + if (res[0].isAdmin) { + context.instance.isAdmin = true; + context.instance.isTutor = false; + context.instance.isStudent = false; + } else if (res[0].isTutor) { + context.instance.isAdmin = false; + context.instance.isTutor = true; + context.instance.isStudent = false; + } else { + context.instance.isAdmin = false; + context.instance.isTutor = false; + context.instance.isStudent = true; + } + + /* Take first and last name from PendingPlatformUser */ + if (res[0].first_name && res[0].name && res[0].semesterId) { + context.instance.first_name = res[0]['first_name']; + context.instance.name = res[0].name; + context.instance.semesterId = res[0].semesterId; + next(); + } else { + var err = new Error("Invalid Data in Whitelist."); + err.statusCode = 500; + next(err); + return; + } + } + }); + + } else { + next(); + } + + }); + + + /** Define an observer that fires after every save operation on the PlatformUsers table. + * If the isTutor flag of an instance has changed, this method reflects the change + * in the RoleMappings table + */ + Platformuser.observe('after save', function (context, next) { + + var app = Platformuser.app; + + if (context.instance) { + + // Search for role object of role 'tutor' + app.models.Role.find( + {where: {or: [{name: 'tutor'}, {name: 'admin'}]}}, + function (err, roles) { + + if (err) throw err; + + var adminRole, tutorRole; + roles.forEach(function (role) { + switch (role.name) { + case 'admin': + adminRole = role; + break; + case 'tutor': + tutorRole = role; + break; + } + }); + + // Search for all role mappings of the saved user to the tutor role + app.models.RoleMapping.find({ + where: { + principalId: context.instance.id, + roleId: tutorRole.id + } + }, + function (err, rolemappings) { + + if (err) throw err; + + // saved user is tutor and role mapping does not exist yet + if (context.instance.isTutor && rolemappings.length === 0) { + + // create new role mapping for user + app.models.RoleMapping.create({ + principalType: 'USER', + principalId: context.instance.id, + roleId: tutorRole.id + }, function (err, newRoleMapping) { + + if (err) throw err; + + }); + + } + + // saved user is not tutor but role mapping still exists + else if (!context.instance.isTutor && rolemappings.length > 0) { + + // delete role mapping for user + var i; + for (i in rolemappings) { + app.models.RoleMapping.destroyById(rolemappings[i].id, function (err) { + + if (err) throw err; + console.log('Deleted RoleMapping'); + + }); + } + } + + // in all other cases we're fine + + }); + + // Search for all role mappings of the saved user to the admin role + app.models.RoleMapping.find({ + where: { + principalId: context.instance.id, + roleId: adminRole.id + } + }, + function (err, rolemappings) { + + if (err) throw err; + + // saved user is admin and role mapping does not exist yet + if (context.instance.isAdmin && rolemappings.length === 0) { + + // create new role mapping for user + app.models.RoleMapping.create({ + principalType: 'USER', + principalId: context.instance.id, + roleId: adminRole.id + }, function (err, newRoleMapping) { + + if (err) throw err; + + }); + + } + + // saved user is not admin but role mapping still exists + else if (!context.instance.isAdmin && rolemappings.length > 0) { + + // delete role mapping for user + var i; + for (i in rolemappings) { + app.models.RoleMapping.destroyById(rolemappings[i].id, function (err) { + + if (err) throw err; + console.log('Deleted RoleMapping'); + + }); + } + } + + // in all other cases we're fine + + }); + }); + + if (context.isNewInstance) { + + // User is registered => Delete from Whitelist + + var PendingPlatformUser = app.models.PendingPlatformUser; + var userMail = context.instance.email; + PendingPlatformUser.destroyAll({email: userMail}, function (err, res) { + if (err) throw err; + }); + } + + } + + next(); + + }); + + + /** + * Defines an observer after a create call. + * Sends a short E-Mail to the user in order to verify the address. + * After the User has clicked the link in the mail, he can is redirected to the 'redirectSwitch' URL. + * The 'redirectSwitch' URL can be set using a Node Variable: + * Windows: set MT_VERIFICATION_REDIRECT=http://www.somedomain.com/login + * Unix: export MT_VERIFICATION_REDIRECT=http://www.somedomain.com/login + * + * If no Node Variable is set "http://localhost:8080/login" is used. + * + * A User can not log in without verifying his address. + * + * The E-Mail server is configured in datasources.json + */ + Platformuser.afterRemote('create', function (context, userInstance, next) { + + var redirectSwitch; + if(process.env.MT_VERIFICATION_REDIRECT!=undefined){ + redirectSwitch=process.env.MT_VERIFICATION_REDIRECT; + } + else { + redirectSwitch="http://localhost:8080/login" + } + + var options = { + type: 'email', + to: userInstance.email, + from: 'noreply@mt.medien.ifi.lmu.de', + subject: 'Medientechnik Registrierung', + redirect: redirectSwitch, + user: userInstance, + templateFn: generateEmail + }; + + userInstance.verify(options, function (err, response, next) { + if (err) return next(err); + + context.res.json({ + title: 'Signed up successfully', + content: 'Please check your email and click on the verification link ' + + 'before logging in.' + }); + }); + + }); + + + /** + * Helper Method for the E-Mail verify routine. Generates an E-Mail with a user-specific verification link + * @param options Options Object provided by PlatformUser.verify + * @param cb Callback + */ + function generateEmail(options, cb) { + + // URL of this app => Works in development and in production + var baseUrl = app.get('url'); + if (process.env.MT_VERIFICATION_BASEURL) { + baseUrl = process.env.MT_VERIFICATION_BASEURL; + } + + Platformuser.findOne({where: {email: options.user.email}}, function (err, res) { + if (err) + cb(err); + var id = res.id + ""; // Stringify the ID + var email = "
Herzlich willkommen zur Plattform des Kurses Medientechnik.
Bevor du dich einloggen kannst, " + + "musst du deine E-Mail-Adresse bestätigen. Klicke dafür unten auf den Link.
Solltest du dich nicht angemeldet " + + "haben, kannst du diese E-Mail ignorieren.

" + + " Bestätige deine E-Mail-Adresse
"; + cb(undefined, email); + + }); + } + + Platformuser.changePassword = function (newPassword, options, cb) { + + var app = Platformuser.app; + var userId = options.accessToken.userId; + + Platformuser.findById(userId, function(err, user){ + if (err) { + cb(err, undefined); + throw err; + } + // PW is hashed automatically by Loopback + user.password = newPassword; + user.save(); + cb(undefined, user); + }) + + }; + + Platformuser.remoteMethod('changePassword', { + accepts: [ + {arg: 'newPassword', type: 'string'}, + {arg: "options", type: "object", http: "optionsFromRequest"} + ], + returns: {arg: 'platformUser', type: 'object'} + }); + + /** + * Adds elements to tutorPossibleLabs relation of a given PlatformUser instance + * + * @param tutorId - ID of a PlatformUser (only isTutor === true allowed). If not specified, + * the ID of the requesting PlatformUser is used + * @param labIds - array of IDs of Lab instances + * @param options - http context + * @param next - callback + * @returns updated list of tutorPossibleLabs + */ + Platformuser.addTutorPossibleLabs = function (tutorId, labIds, options, next) { + + var Lab = Platformuser.app.models.Lab, + userId = tutorId, + affectedUser, + affectedUserPossibleLabs, + affectedLabs; + + if (!userId) { + userId = options.accessToken.userId; + } + + Platformuser.find({where: {id: userId}}, function (err, userSearchResult) { + + if (err) throw err; + if (userSearchResult.length === 0) { + next(new Error('No PlatformUser instance with id '+userId+' found'), null); + return; + } + + affectedUser = userSearchResult[0]; + + if (!affectedUser.isTutor) { + next(new Error('PlatformUser with id '+userId+' is not a tutor'), null); + return; + } + + Lab.find({where: {id: {inq: labIds}}}, function (err, labSearchResult) { + + if (err) throw err; + if (labSearchResult.length === 0) { + next(new Error('No Lab instances with specified ids found'), null); + return; + } + + affectedLabs = labSearchResult.slice(); + + affectedUser.__get__tutorPossibleLabs(function(err, possibleLabs){ + + if (err) throw err; + affectedUserPossibleLabs = possibleLabs.slice(); + var affectedUserPossibleLabsAsStrings = affectedUserPossibleLabs.map(function(idObject){ + return idObject.toString(); + }); + affectedLabs.forEach(function(affectedLab){ + if (affectedUserPossibleLabsAsStrings.indexOf(affectedLab.id.toString()) < 0) { + affectedUserPossibleLabs.push(affectedLab.id); + } + }); + + Platformuser.updateAll({id: userId}, {tutorPossibleLabs: affectedUserPossibleLabs}, function(err, info){ + + if (err) throw err; + if (info.count > 1) { + next(new Error('Warning: Update affected more than one PlatformUser instance (' + +info.count+' total)'), null); + return; + } + if (info.count < 1) { + next(new Error('No update performed'), null); + return; + } + + next(null, affectedUserPossibleLabs); + + }); + }); + }); + }); + }; + + Platformuser.remoteMethod('addTutorPossibleLabs', { + accepts: [ + {arg: 'tutorId', type: 'string'}, + {arg: 'labIds', type: 'array'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest'} + ], + returns: [ + {type: 'object', root: true} + ] + }); + + /** + * Deletes tutorPossibleLab references for a given PlatformUser. If a labTypeId is specified, + * deletes all references to Labs that belong to this LabType. Otherwise removes all + * references to Labs specified in the labIds array. + * + * @param tutorId - ID of a PlatformUser (only isTutor === true allowed). If not specified, + * the ID of the requesting PlatformUser is used + * @param labTypeId - ID of a LabType instance + * @param labIds - array of IDs of Lab instances + * @param options - http context + * @param next - callback + * @returns updated list of tutorPossibleLabs + */ + Platformuser.removeTutorPossibleLabs = function (tutorId, labTypeId, labIds, options, next) { + + var Lab = Platformuser.app.models.Lab, + userId = tutorId, + affectedUser, + affectedLabs, + tutorPossibleLabs; + + if (!userId) { + userId = options.accessToken.userId; + } + + Platformuser.find({where: {id: userId}}, function (err, userSearchResult) { + + if (err) throw err; + if (userSearchResult.length === 0) { + next(new Error('No PlatformUser instance with id ' + userId + ' found'), null); + return; + } + + affectedUser = userSearchResult[0]; + + if (!affectedUser.isTutor) { + next(new Error('PlatformUser with id ' + userId + ' is not a tutor'), null); + return; + } + + var query = (labTypeId) ? {labTypeId: labTypeId} : {id: {inq: labIds}}; + + Lab.find({where: query}, function(err, labSearchResult) { + + if (err) throw err; + if (labSearchResult.length === 0) { + next(new Error('No Labs found with given criteria. Nothing to delete.'), null); + return; + } + + affectedLabs = labSearchResult.slice(); + + affectedUser.__get__tutorPossibleLabs(function(err, possibleLabs){ + + if (err) throw err; + tutorPossibleLabs = possibleLabs; + var labIdsForLabType = affectedLabs.map(function(lab){ + return lab.id.toString(); + }); + var reducedTutorPossibleLabs = tutorPossibleLabs.filter(function(idObject){ + return (labIdsForLabType.indexOf(idObject.toString()) < 0); + }); + + Platformuser.updateAll({id: userId}, {tutorPossibleLabs: reducedTutorPossibleLabs}, function(err, info){ + + if (err) throw err; + if (info.count > 1) { + next(new Error('Warning: Update affected more than one PlatformUser instance (' + +info.count+' total)'), null); + return; + } + if (info.count < 1) { + next(new Error('No update performed'), null); + return; + } + + next(null, reducedTutorPossibleLabs); + }); + }); + }); + }); + }; + + Platformuser.remoteMethod('removeTutorPossibleLabs', { + accepts: [ + {arg: 'tutorId', type: 'string'}, + {arg: 'labTypeId', type: 'string'}, + {arg: 'labIds', type: 'array'}, + {arg: 'options', type: 'object', http: 'optionsFromRequest' } + ], + returns: [ + {type: 'object', root: true} + ] + }); + + Platformuser.setPassedLabType = function (userId, labTypeId, passed, next) { + + var affectedUser, affectedLabType, + LabType = Platformuser.app.models.LabType; + + // get user + Platformuser.find({where: {id: userId}}, function (err, userSearchResult) { + + if (err) throw err; + if (userSearchResult.length === 0) { + next(new Error('No PlatformUser instance with id '+userId+' found'), null); + return; + } + + affectedUser = userSearchResult[0]; + + // get labType + LabType.find({where: {id: labTypeId}}, function(err, labTypeSearchResult){ + + if (err) throw err; + if (labTypeSearchResult.length === 0) { + next(new Error('No LabType instance with id '+labTypeId+' found'), null); + return; + } + + affectedLabType = labTypeSearchResult[0]; + + // check if user has passed this LabType already + var passedLabTypes = affectedUser.passedLabTypesIds; + var passedLabTypesAsStrings = passedLabTypes.map(function(id){ + return id.toString(); + }); + var searchIndex = passedLabTypesAsStrings.indexOf(labTypeId); + // adjust relation accordingly + if (passed && searchIndex < 0) { + // User has passed the lab but is not yet stored in his passedLabs + passedLabTypes.push(affectedLabType.id); + } else if (!passed && searchIndex >= 0) { + // User has not passed the lab but it is still stored in his passedLabs + passedLabTypes.splice(searchIndex, 1); + } + + // store user + affectedUser.updateAttribute('passedLabTypesIds', passedLabTypes, function(err, instance) { + + if (err) throw err; + next(null, instance); + + }); + }); + }); + }; + + Platformuser.remoteMethod('setPassedLabType', { + accepts: [ + {arg: 'userId', type: 'string'}, + {arg: 'labTypeId', type: 'string'}, + {arg: 'passed', type: 'boolean'} + ], + returns: [ + {type: 'object', root: true} + ] + }); + +}; + + diff --git a/common/models/platform-user.json b/common/models/platform-user.json new file mode 100644 index 0000000..abd6e2e --- /dev/null +++ b/common/models/platform-user.json @@ -0,0 +1,120 @@ +{ + "name": "PlatformUser", + "base": "User", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "name": { + "type": "string", + "required": true + }, + "first_name": { + "type": "string", + "required": true + }, + "isAdmin": { + "type": "boolean", + "required": true, + "default": false + }, + "isTutor": { + "type": "boolean", + "required": true, + "default": false + }, + "isStudent": { + "type": "boolean", + "required": false, + "default": true + }, + "free_tutor_dates": { + "type": [ + "date" + ] + } + }, + "validations": [], + "relations": { + "uniqueDates": { + "type": "referencesMany", + "model": "UniqueDates", + "foreignKey": "uniqueDatesIds" + }, + "semester": { + "type": "belongsTo", + "model": "Semester", + "foreignKey": "semesterId" + }, + "group": { + "type": "belongsTo", + "model": "Group", + "foreignKey": "groupId" + }, + "exercise": { + "type": "referencesMany", + "model": "Exercise", + "foreignKey": "exerciseIds" + }, + "tutorPossibleLabs": { + "type": "hasAndBelongsToMany", + "model": "Lab", + "foreignKey": "" + }, + "passedLabTypes": { + "type": "referencesMany", + "model": "LabType", + "foreignKey": "passedLabTypesIds" + } + }, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW", + "property": "addTutorPossibleLabs" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW", + "property": "removeTutorPossibleLabs" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW", + "property": "setPassedLabType" + }, + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW" + }, + { + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW", + "property": "changePassword" + } + ], + "methods": {} +} diff --git a/common/models/priority.js b/common/models/priority.js new file mode 100644 index 0000000..0bbb59b --- /dev/null +++ b/common/models/priority.js @@ -0,0 +1,132 @@ +'use strict'; +var app = require('../../server/server'); +module.exports = function (Priority) { + + Priority.observe('before save', function (context, next) { + + if (context.instance && context.instance.labId) { + + Priority.app.models.Lab.find( + {where: {id: context.instance.labId}}, + function (err, labSearchResult) { + + if (err) throw err; + + if (labSearchResult.length > 0) { + + context.instance.labTypeId = labSearchResult[0].labTypeId; + + next(); + + } else { + + next(new Error("Lab with id " + context.instance.labId + " not found")); + + } + + }); + + } else { + + next(new Error("Instances of Priority model must have a member labId")); + + } + + }); + + /** + * This observer adds the Priority ID to the priorityIds field in the Group provided in the groupId field of + * the Priority + */ + Priority.observe('after save', function (context, next) { + + var currentInstance = null; + if (context.currentInstance) { + currentInstance = context.currentInstance; + } + else if (context.instance) { + currentInstance = context.instance; + } + + if (currentInstance != undefined && currentInstance != null) { + currentInstance.__get__group(function (err, group) { + + if (err) throw err; + + // Linking Prio to Group: + group.priorities.add(currentInstance.id, function (err, res) { + if (err) throw err; + next(); + + }) + + }) + } + + else { + next(); + } + }); + + /** + * This observer removes the Priority ID from the priorityIds field in the Group provided in the groupId field of + * the Priority + */ + Priority.observe('before delete', function (context, next) { + + Priority.findById(context.where.id, function (err, currentInstance) { + + if (currentInstance != undefined && currentInstance != null) { + currentInstance.__get__group(function (err, group) { + if (err) throw err; + // Unlinking Prio from Group + group.priorities.remove(context.where.id, function (err, res) { + if (err) throw err; + next(); + }); + }) + } + + else { + next(); + } + + }); + + }); + + Priority.deleteByGroupAndLabType = function (groupId, labTypeId, next) { + + if (!groupId || !labTypeId) { + next(new Error('groupId and labTypeId must be specified'), null); + return; + } + + Priority.destroyAll( + {and: [ + {groupId: groupId}, + {labTypeId: labTypeId} + ]}, + function(err, info){ + if (err) { + next(err, null); + return; + } + next(null, info); + }); + }; + + Priority.remoteMethod('deleteByGroupAndLabType', { + http: { + verb: 'del' + }, + accepts: [ + {arg: 'groupId', type: 'string'}, + {arg: 'labTypeId', type: 'string'} + ], + returns: [ + {arg: 'count', type: 'number'} + ] + }); + +}; diff --git a/common/models/priority.json b/common/models/priority.json new file mode 100644 index 0000000..31123df --- /dev/null +++ b/common/models/priority.json @@ -0,0 +1,54 @@ +{ + "name": "Priority", + "plural": "Priorities", + "base": "PersistedModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "priority": { + "type": "number", + "required": true + } + }, + "validations": [], + "relations": { + "group": { + "type": "belongsTo", + "model": "Group", + "foreignKey": "groupId" + }, + "lab": { + "type": "belongsTo", + "model": "Lab", + "foreignKey": "labId" + }, + "labType": { + "type": "belongsTo", + "model": "LabType", + "foreignKey": "labTypeId" + }, + "semester": { + "type": "belongsTo", + "model": "Semester", + "foreignKey": "semesterId" + } + }, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW" + } + ], + "methods": {} +} diff --git a/common/models/semester.js b/common/models/semester.js new file mode 100644 index 0000000..9cfd800 --- /dev/null +++ b/common/models/semester.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function(Semester) { + +}; diff --git a/common/models/semester.json b/common/models/semester.json new file mode 100644 index 0000000..115f65e --- /dev/null +++ b/common/models/semester.json @@ -0,0 +1,51 @@ +{ + "name": "Semester", + "base": "PersistedModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "term": { + "type": "string", + "required": true + }, + "start_date": { + "type": "date", + "required": true + }, + "end_date": { + "type": "date", + "required": true + }, + "group_size": { + "type": "number", + "required": true, + "default": 4 + } + }, + "validations": [], + "relations": {}, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + }, + { + "accessType": "WRITE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW" + } + ], + "methods": {} +} diff --git a/common/models/unique-date.js b/common/models/unique-date.js new file mode 100644 index 0000000..8c85384 --- /dev/null +++ b/common/models/unique-date.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function(Uniquedate) { + +}; diff --git a/common/models/unique-date.json b/common/models/unique-date.json new file mode 100644 index 0000000..106beda --- /dev/null +++ b/common/models/unique-date.json @@ -0,0 +1,57 @@ +{ + "name": "UniqueDate", + "base": "PersistedModel", + "idInjection": true, + "options": { + "validateUpsert": true + }, + "forceId": false, + "properties": { + "name": { + "type": "string", + "required": true + }, + "date": { + "type": "date", + "required": true + }, + "location": { + "type": "string" + }, + "duration": { + "type": "number" + }, + "description": { + "type": "string" + } + }, + "validations": [], + "relations": { + "semester": { + "type": "belongsTo", + "model": "Semester", + "foreignKey": "semesterId" + } + }, + "acls": [ + { + "accessType": "*", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "DENY" + }, + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$authenticated", + "permission": "ALLOW" + }, + { + "accessType": "WRITE", + "principalType": "ROLE", + "principalId": "admin", + "permission": "ALLOW" + } + ], + "methods": {} +} diff --git a/frontend/index.html b/frontend/index.html index da43fed..a862ee8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -85,11 +85,11 @@ })(); // Load pre-caching Service Worker - if ('serviceWorker' in navigator) { + /*if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('/service-worker.js'); }); - } + }*/ diff --git a/frontend/src/services/api-request.html b/frontend/src/services/api-request.html index a5d1fcf..7d6b3a8 100644 --- a/frontend/src/services/api-request.html +++ b/frontend/src/services/api-request.html @@ -29,8 +29,8 @@ baseUrl: { type: String, reflectToAttribute: false, - value: 'http://localhost:3000/api' - //value: 'https://lmu-mt-api.herokuapp.com/api' + //value: 'http://localhost:3000/api' + value: 'https://lmu-mt.herokuapp.com/api' }, body: { @@ -102,4 +102,4 @@ - \ No newline at end of file + diff --git a/frontend/src/shared/locales.json b/frontend/src/shared/locales.json index 7a260d9..94852c0 100644 --- a/frontend/src/shared/locales.json +++ b/frontend/src/shared/locales.json @@ -6,7 +6,7 @@ "logincard_locale button tooltip": "Change to German", "logincard_login button": "Login", "logincard_password placeholder": "Password", - "logincard_register text": "New here?", + "logincard_register text": "Are you new here?", "logincard_register button": "Register", "groupdetail_create group": "Create new group", diff --git a/frontend/sw-precache-config.js b/frontend/sw-precache-config.js index cd20c76..29f0a4c 100644 --- a/frontend/sw-precache-config.js +++ b/frontend/sw-precache-config.js @@ -7,11 +7,11 @@ * Code distributed by Google as part of the polymer project is also * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ -module.exports = { +/*module.exports = { staticFileGlobs: [ '/index.html', '/manifest.json', '/bower_components/webcomponentsjs/webcomponents-lite.min.js' ], navigateFallback: '/index.html' -}; +};*/ diff --git a/package.json b/package.json index 0bf5ae3..250c4a8 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,34 @@ { - "name": "pwd-mt", - "version": "0.0.0", - "private": true, + "name": "mt-common-api", + "version": "1.0.0", + "main": "server/server.js", "scripts": { - "start": "node ./bin/www", - "postinstall": "cd frontend && bower install && polymer build && cd .." + "lint": "eslint .", + "start": "node .", + "postinstall": "cd frontend && bower install && polymer build && cd ..", + "posttest": "npm run lint && nsp check" }, "dependencies": { - "body-parser": "~1.15.2", - "bower": "^1.8.0", - "cookie-parser": "~1.4.3", - "debug": "~2.2.0", - "express": "~4.14.0", - "jade": "~1.11.0", - "morgan": "~1.7.0", - "polymer-cli": "^0.17.0", - "serve-favicon": "~2.3.0" + "compression": "^1.0.3", + "cors": "^2.5.2", + "helmet": "^1.3.0", + "loopback": "^3.0.0", + "loopback-boot": "^2.6.5", + "loopback-component-explorer": "^2.4.0", + "loopback-connector-mongodb": "^1.17.0", + "loopback-datasource-juggler": "^2.39.0", + "serve-favicon": "^2.0.1", + "strong-error-handler": "^1.0.1" }, "devDependencies": { - "gulp": "^3.9.1" - } + "eslint": "^2.13.1", + "eslint-config-loopback": "^4.0.0", + "nsp": "^2.1.0" + }, + "repository": { + "type": "", + "url": "" + }, + "license": "UNLICENSED", + "description": "mt-common-api" } diff --git a/routes/index.js b/routes/index.js deleted file mode 100644 index ecca96a..0000000 --- a/routes/index.js +++ /dev/null @@ -1,9 +0,0 @@ -var express = require('express'); -var router = express.Router(); - -/* GET home page. */ -router.get('/', function(req, res, next) { - res.render('index', { title: 'Express' }); -}); - -module.exports = router; diff --git a/server/boot/authentication.js b/server/boot/authentication.js new file mode 100644 index 0000000..8e88d4b --- /dev/null +++ b/server/boot/authentication.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = function enableAuthentication(server) { + // enable authentication + server.enableAuth(); +}; diff --git a/server/boot/seed.js b/server/boot/seed.js new file mode 100644 index 0000000..b3db265 --- /dev/null +++ b/server/boot/seed.js @@ -0,0 +1,1023 @@ +var semesters = []; +var uniqueDates = []; +var roles = []; +var platformUsers = []; +var groups = []; +var labtypes = []; +var labs = []; +var priorities = []; + +module.exports = function (app) { + + // SEMESTER + app.dataSources.mongo.automigrate('Semester', function (err) { + if (err) throw err; + + console.log('\nSeeding DB with dummy data...'); + + app.models.Semester.create([{ + "term": "WS 2016/2017", + "start_date": "2016-09-10", + "end_date": "2017-02-20", + "group_size": 4 + }, { + "term": "SS 2016", + "start_date": "2016-04-01", + "end_date": "2016-09-09", + "group_size": 3 + }], function (err, documents) { + if (err) throw err; + console.log('Semester models created'); + semesters = semesters.concat(documents); + seedUniqueDates(app); + }); + }); +}; + +// UNIQUE DATES +seedUniqueDates = function (app) { + app.dataSources.mongo.automigrate('UniqueDate', function (err) { + if (err) throw err; + + var testDate = new Date("February 10, 2017 11:15:00"); + var testDate2 = new Date("March 15, 2017 11:15:00"); + var testDate3 = new Date("March 16, 2017 11:15:00"); + var testDate4 = new Date("March 17, 2017 11:15:00"); + var testDate5 = new Date("March 29, 2017 11:15:00"); + var testDate6 = new Date("April 30, 2017 11:15:00"); + app.models.UniqueDate.create([ + { + "name": "Klausur", + "date": testDate, + "location": "Amalienstraße 67, Raum 001", + "duration": "90", + "description": "Semester Klausur WS 2016/2017", + "semesterId": semesters[0].id + },{ + "name": "Wäsche waschen", + "date": testDate6, + "location": "Amalienstraße 67, Raum 001", + "duration": "90", + "description": "Semester Klausur WS 2016/2017", + "semesterId": semesters[0].id + },{ + "name": "Oscarverleihung", + "date": testDate2, + "location": "Amalienstraße 67, Raum 001", + "duration": "90", + "description": "Semester Klausur WS 2016/2017", + "semesterId": semesters[0].id + },{ + "name": "Klassenfahrt", + "date": testDate3, + "location": "Amalienstraße 67, Raum 001", + "duration": "90", + "description": "Semester Klausur WS 2016/2017", + "semesterId": semesters[0].id + },{ + "name": "Abgabe Fotoprojekt", + "date": testDate5, + "location": "Amalienstraße 67, Raum 001", + "duration": "90", + "description": "Semester Klausur WS 2016/2017", + "semesterId": semesters[0].id + },{ + "name": "Endlich Ferien", + "date": testDate4, + "location": "Amalienstraße 67, Raum 001", + "duration": "90", + "description": "Semester Klausur WS 2016/2017", + "semesterId": semesters[0].id + } + ], function (err, documents) { + if (err) throw err; + console.log('Unique date models created'); + uniqueDates = uniqueDates.concat(documents); + seedRoles(app); + }); + }); +}; + +// ROLES +seedRoles = function (app) { + app.dataSources.mongo.automigrate('Role', function (err) { + + if (err) throw err; + + app.models.Role.create([ + {name: 'tutor'}, + {name: 'admin'} + ], function (err, documents) { + + if (err) throw err; + + console.log('User roles created'); + roles = roles.concat(documents); + seedPendingPlatformUsers(app, documents[1]); + + }); + }); +}; + + +// PENDING PLATFORM USER +seedPendingPlatformUsers = function (app, adminRole) { + app.dataSources.mongo.automigrate('PlatformUser', function (err) { + + app.dataSources.mongo.automigrate('PendingPlatformUser', function (err) { + if (err) throw err; + + var semesterOneId = semesters[0].id; + var semesterTwoId = semesters[1].id; + + app.models.PendingPlatformUser.create([ + + // DEFAULT ADMIN + { + "name": "Medientechnik", + "first_name": "Administrator", + "isAdmin": true, + "isTutor": true, + "email": "admin@mt.medien.ifi.lmu.de", + "semesterId": semesterOneId + }, + + // SEMESTER ONE - Group 1 Members + { + "name": "Mustermann", + "first_name": "Max", + "isAdmin": false, + "isTutor": false, + "email": "max.mustermann@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "Wurst", + "first_name": "Hans", + "isAdmin": false, + "isTutor": false, + "email": "wursth@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "Meier", + "first_name": "Jeremy-Pascal", + "isAdmin": false, + "isTutor": false, + "email": "jeremypascal@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "Krüger", + "first_name": "Dakota ", + "isAdmin": false, + "isTutor": false, + "email": "dakota@campus.lmu.de", + "semesterId": semesterOneId + }, + + // SEMESTER ONE - Registered users without a group + { + "name": "Meier", + "first_name": "Josef-Jakob", + "isAdmin": false, + "isTutor": false, + "email": "josefjakob@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "Huber", + "first_name": "Jacqueline", + "isAdmin": false, + "isTutor": false, + "email": "jacky@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "Meininger", + "first_name": "Orlando", + "isAdmin": false, + "isTutor": false, + "username": "orlando@campus.lmu.de", + "email": "orlando@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "di Lorenzo", + "first_name": "Gianna", + "isAdmin": false, + "isTutor": false, + "email": "gianna@campus.lmu.de", + "semesterId": semesterOneId + }, + + // SEMESTER ONE - Pending users + { + "first_name": "Justin", + "name": "Krämer", + "email": "justin@campus.lmu.de", + "isTutor": false, + "semesterId": semesterOneId + }, { + "first_name": "Newt", + "name": "Utor", + "email": "newTutor@campus.lmu.de", + "isTutor": true, + "semesterId": semesterOneId + }, { + "first_name": "Adalbert", + "name": "Minh", + "email": "ad.minh@ifi.lmu.de", + "isAdmin": true, + "semesterId": semesterOneId + }, + + // SEMESTER ONE - Tutors + { + "name": "Dombrowsky", + "first_name": "Kevin", + "isAdmin": false, + "isTutor": true, + "email": "kevin@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "Schmidt", + "first_name": "Wilhelmina", + "isAdmin": false, + "isTutor": true, + "email": "willischmidt@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "Herberger", + "first_name": "Sepp", + "isAdmin": false, + "isTutor": true, + "email": "sepp@campus.lmu.de", + "semesterId": semesterOneId + }, + { + "name": "Mittermeier", + "first_name": "Rosi", + "isAdmin": false, + "isTutor": true, + "email": "rosi@campus.lmu.de", + "semesterId": semesterOneId + }, + + // SEMESTER TWO - Pending users + { + "name": "Himmel", + "first_name": "Jens", + "isAdmin": false, + "isTutor": false, + "email": "jens.himmel@campus.lmu.de", + "semesterId": semesterTwoId + }, + { + "name": "Egger", + "first_name": "Jennifer", + "isAdmin": false, + "isTutor": false, + "email": "eggerj@campus.lmu.de", + "semesterId": semesterTwoId + }, + { + "name": "Weiss", + "first_name": "Benjamin", + "isAdmin": false, + "isTutor": false, + "email": "weiss.b@campus.lmu.de", + "semesterId": semesterTwoId + }, + { + "name": "Möller", + "first_name": "Manuela", + "isAdmin": false, + "isTutor": false, + "email": "moeller.manuela@campus.lmu.de", + "semesterId": semesterTwoId + }, + { + "name": "Brandt", + "first_name": "Bernd", + "isAdmin": false, + "isTutor": false, + "email": "bbrandt@campus.lmu.de", + "semesterId": semesterTwoId + } + ], function (err, documents) { + + if (err) throw err; + console.log('PendingPlatformUser models created'); + + seedPlatformUsers(app, adminRole); + + }); + }); + }); +}; + + +// PlatformUsers +seedPlatformUsers = function (app, adminrole) { + app.dataSources.mongo.automigrate('PlatformUser', function (err) { + if (err) throw err; + + var testDate = new Date("December 12, 2016 11:30:00"); + var dateIds = []; + dateIds.push(uniqueDates[0].id); + + var tutorFreeDatesTest = []; + tutorFreeDatesTest.push(testDate); + + app.models.PlatformUser.create( + [ + /* This is the default admin user and should not be + * deleted when going in production */ + { + "name": "Medientechnik", + "first_name": "Administrator", + "isAdmin": true, + "isTutor": true, + "password": "mtadmin", + "username": "mtadmin", + "email": "admin@mt.medien.ifi.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "uniqueDateIds": [], + "semesterId": "" + }, + + // SEMESTER ONE - Members of Group 1 + { + "name": "Mustermann", + "first_name": "Max", + "isAdmin": false, + "isTutor": false, + "password": "test", + "username": "max.mustermann@campus.lmu.de", + "email": "max.mustermann@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "uniqueDateIds": dateIds, + "semesterId": semesters[0].id + }, + { + "name": "Wurst", + "first_name": "Hans", + "isAdmin": false, + "isTutor": false, + "password": "test", + "username": "wursth@campus.lmu.de", + "email": "wursth@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + + }, + { + "name": "Meier", + "first_name": "Jeremy-Pascal", + "isAdmin": false, + "isTutor": false, + "password": "test", + "username": "jeremypascal@campus.lmu.de", + "email": "jeremypascal@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + { + "name": "Krüger", + "first_name": "Dakota ", + "isAdmin": false, + "isTutor": false, + "password": "test", + "username": "dakota@campus.lmu.de", + "email": "dakota@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + + // SEMESTER ONE - Tutors + { + "name": "Dombrowsky", + "first_name": "Kevin", + "isAdmin": false, + "isTutor": true, + "free_tutor_dates": tutorFreeDatesTest, + "password": "test", + "username": "kevin@campus.lmu.de", + "email": "kevin@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + { + "name": "Schmidt", + "first_name": "Wilhelmina", + "isAdmin": false, + "isTutor": true, + "password": "test", + "username": "willischmidt@campus.lmu.de", + "email": "willischmidt@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + { + "name": "Herberger", + "first_name": "Sepp", + "isAdmin": false, + "isTutor": true, + "password": "test", + "username": "sepp@campus.lmu.de", + "email": "sepp@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + { + "name": "Mittermeier", + "first_name": "Rosi", + "isAdmin": false, + "isTutor": true, + "password": "test", + "username": "rosi@campus.lmu.de", + "email": "rosi@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + + // SEMESTER ONE - Registered users without a group + { + "name": "Meier", + "first_name": "Josef-Jakob", + "isAdmin": false, + "isTutor": false, + "password": "test", + "username": "josefjakob@campus.lmu.de", + "email": "josefjakob@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + { + "name": "Huber", + "first_name": "Jacqueline", + "isAdmin": false, + "isTutor": false, + "password": "test", + "username": "jacky@campus.lmu.de", + "email": "jacky@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + { + "name": "Meininger", + "first_name": "Orlando", + "isAdmin": false, + "isTutor": false, + "password": "test", + "username": "orlando@campus.lmu.de", + "email": "orlando@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + }, + { + "name": "di Lorenzo", + "first_name": "Gianna", + "isAdmin": false, + "isTutor": false, + "password": "test", + "username": "gianna@campus.lmu.de", + "email": "gianna@campus.lmu.de", + "emailVerified": true, + "created": testDate, + "lastUpdated": testDate, + "semesterId": semesters[0].id + } + + ], function (err, documents) { + if (err) throw err; + console.log('User models created'); + platformUsers = platformUsers.concat(documents); + + /* Assign admin role to default admin user. + * Do not delete when going in production. */ + /*adminrole.principals.create({ + principalType: app.models.RoleMapping.USER, + principalId: documents[0].id + }, function (err, principal) { + if (err) throw err; + console.log('Assigned admin role');*/ + seedExercise(app); + //}); + }); + }); +}; + + +// EXERCISE +seedExercise = function (app) { + app.dataSources.mongo.automigrate('Exercise', function (err) { + if (err) throw err; + + var members = []; + members.push(platformUsers[0].id); + members.push(platformUsers[1].id); + members.push(platformUsers[2].id); + members.push(platformUsers[3].id); + var testDate = new Date("December 12, 2016 11:30:00"); + app.models.Exercise.create([ + { + "name": "Übungsgruppe 1", + "date": testDate, + "semesterId": semesters[0].id, + "location": "Amalienstraße 17", + "participantsUserIds": members + }, { + "name": "Übungsgruppe 1", + "date": testDate, + "semesterId": semesters[0].id, + "location": "Amalienstraße 17", + "participantsUserIds": [] + } + ], function (err, documents) { + if (err) throw err; + console.log('Exercise models created'); + seedGroups(app); + }); + }); +}; + + +// GROUP +seedGroups = function (app) { + app.dataSources.mongo.automigrate('Group', function (err) { + if (err) throw err; + + var members = []; + members.push(platformUsers[1].id); + members.push(platformUsers[2].id); + members.push(platformUsers[3].id); + members.push(platformUsers[4].id); + + var labIds; + + app.models.Group.create({ + "name": "Medienchaoten", + "groupMemberIds": members, + "semesterId": semesters[0].id + }, function (err, documents) { + if (err) throw err; + console.log('Group models created'); + groups = groups.concat(documents); + + for (var i = 0; i < members.length; i++) { + + app.models.PlatformUser.updateAll( + {"id": platformUsers[i + 1].id}, + {"groupId": groups[0].id}, + function (err, info) { + if (err) throw err; + // Do nothing - should be ok to update this async + }); + } + + seedLabType(app); + + }); + }); +}; + + +// LABTYPE +seedLabType = function (app) { + app.dataSources.mongo.automigrate('LabType', function (err) { + if (err) throw err; + var testDate = new Date("April 14, 2017 11:26:00"); + var testDate2 = new Date("April 30, 2017 18:30:00"); + var testDate3 = new Date("May 14, 2017 18:30:00"); + var testDate4 = new Date("May 30, 2017 18:30:00"); + var testDate5 = new Date("June 14, 2017 18:30:00"); + var testDate6 = new Date("June 30, 2017 18:30:00"); + app.models.LabType.create([ + { + "type": "1", + "type_str": "Foto", + "registration_open": true, + "registration_deadline": testDate2, + "registration_deadline_tutors": testDate, + "description_tutor": "Beschreibung Tutor", + "description_student": "Beschreibung Student", + "semesterId": semesters[0].id + }, + { + "type": "2", + "type_str": "Video", + "registration_open": true, + "registration_deadline": testDate4, + "registration_deadline_tutors": testDate3, + "description_tutor": "Beschreibung Tutor", + "description_student": "Beschreibung Student", + "semesterId": semesters[0].id + }, + { + "type": "3", + "type_str": "Audio", + "registration_open": true, + "registration_deadline": testDate6, + "registration_deadline_tutors": testDate5, + "description_tutor": "Beschreibung Tutor", + "description_student": "Beschreibung Student", + "semesterId": semesters[0].id + }, + { + "type": "4", + "type_str": "Videoschnitt", + "registration_open": true, + "registration_deadline": testDate4, + "registration_deadline_tutors": testDate3, + "description_tutor": "Beschreibung Tutor", + "description_student": "Beschreibung Student", + "semesterId": semesters[0].id + } + ], function (err, documents) { + if (err) throw err; + console.log('LabType models created'); + labtypes = labtypes.concat(documents); + seedLab(app); + + }); + }); +}; + +// LAB +seedLab = function (app) { + app.dataSources.mongo.automigrate('Lab', function (err) { + if (err) throw err; + var testDate = new Date("October 31, 2016 16:00:00"); + + var labEntities = []; + labEntities.unshift({ + "date": testDate, + "duration": "120", + "location": "Amalienstraße 17", + "passed": false, + "groupId": groups[0].id, + "labTypeId": labtypes[0].id, + "semesterId": semesters[0].id + }); + + app.models.Lab.create(labEntities, function (err, documents) { + if (err) throw err; + console.log('Lab models created'); + + labs = labs.concat(documents); + var labIds = labs.map(function (lab) { + return lab.id; + }); + + // Statically create Relation sample group to sample lab + app.models.Group.updateAll({"id": groups[0].id}, {"labIds": [labIds[0]]}, function (err, info) { + if (err) throw err; + // This happens asynchronously + }); + + // Assume all goes well in the for each loop above and continue + seedPriority(app); + + }); + }); +}; + +// PRIORITY +seedPriority = function (app) { + app.dataSources.mongo.automigrate('Priority', function (err) { + if (err) throw err; + + app.models.Priority.create({ + "priority": 1, + "groupId": groups[0].id, + "labId": labs[0].id, + "semesterId": semesters[0].id + }, function (err, documents) { + if (err) throw err; + console.log('Priority models created'); + + priorities.push(documents); + + var testPriorities = []; + testPriorities.push(documents.id); + + // Add a priority to a group + app.models.Group.updateAll({"id": groups[0].id}, {"priorityIds": testPriorities}, function (err, info) { + + generateSemesterScenario(app); + + }); + + }); + }); +}; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* * Generate large scale test data for distribution algorithm * */ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +/** + * Returns a random subset of an array + * @param array + * @returns {Array} + */ +function randomSubset(array) { + var subsetLength = Math.floor(Math.random() * array.length); + var index = 0, subset = []; + while (index < subsetLength) { + var nextSubsetElement = array[Math.floor(Math.random() * array.length)]; + while (subset.indexOf(nextSubsetElement) >= 0) { + nextSubsetElement = array[Math.floor(Math.random() * array.length)]; + } + subset.push(nextSubsetElement); + index++; + } + return subset; +} + +/** + * Generates a random sequence of latin letters. Length ranges from 4 to 15 letters. + * + * @param {boolean} uppercase - start with an uppercase letter + * @returns {string} + */ +function randomName(capitalize) { + var uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + var lowercase = 'abcdefghijklmnopqrstuvwxyz'; + var numLowercaseLetters = Math.floor(Math.random() * 10 + 4); + var name = ''; + if (capitalize) { + name += uppercase.charAt(Math.floor(Math.random() * uppercase.length)); + } + var index = 0; + while (index < numLowercaseLetters) { + name += lowercase.charAt(Math.floor(Math.random() * lowercase.length)); + index++; + } + return name; +} + +/** + * Generates a list of random PlatformUser-like objects representing a set of students + * + * @param numberOfStudents - number of students to be created + * @param semesterId - ID of semester that students will belong to + */ +function generateStudents(numberOfStudents, semesterId) { + var index = 0, students = []; + while (index < numberOfStudents) { + students.push({ + "name": randomName(true), + "first_name": randomName(true), + "email": randomName(false) + '@campus.lmu.de', + "semesterId": semesterId + }); + index++; + } + return students; +} + +/** + * Generates a list of Priority-like objects representing a random choice of labs for a given group + * + * @param group - the group instance for which the priorities are generated + * @param labIds - a list of possible choices for the group + * @param numberOfPriorities - number of priorities that will be chosen + * @returns {Array} + */ +function generateRandomPriorities(group, labIds, numberOfPriorities) { + if (numberOfPriorities > labIds.length) { + throw new Error('numberOfPriorities (' + numberOfPriorities + ') out of bounds. Cannot choose more ' + + 'priorities than labIds provided (' + labIds.length + ')'); + return; + } + var index = 0, chosenLabIds = []; + while (index < numberOfPriorities) { + var nextLabId = labIds[Math.floor(Math.random() * labIds.length)]; + while (chosenLabIds.indexOf(nextLabId) >= 0) { + nextLabId = labIds[Math.floor(Math.random() * labIds.length)]; + } + chosenLabIds.push(nextLabId); + index++; + } + return chosenLabIds.map(function (id, index) { + return { + groupId: group.id, + labId: id, + priority: index, + semesterId: group.semesterId + }; + }); +} + +generateSemesterScenario = function (app) { + + var scenarioSemester = semesters[0], + scenarioStudents, + scenarioGroups; + + console.log('\nGenerating semester scenario for semester ' + scenarioSemester.id + '...'); + + // Generate Labs + var testDatesPhoto = [], testDatesVideo = [], testDatesCutting = [], testDatesAudio = []; + var iteratorDatePhoto = new Date(2017, 4, 1, 8); // May 1, 2017 8:00:00 + var iteratorDateVideo = new Date(2017, 5, 1, 8); // June 1, 2017 8:00:00 + var iteratorDateCutting = new Date(2017, 5, 1, 8); // June 1, 2017 8:00:00 + var iteratorDateAudio = new Date(2017, 6, 1, 8); // July 1, 2017 8:00:00 + for (var i = 0; i < 20; i++) { + testDatesPhoto[i] = new Date(iteratorDatePhoto); + testDatesVideo[i] = new Date(iteratorDateVideo); + testDatesCutting[i] = new Date(iteratorDateCutting); + testDatesAudio[i] = new Date(iteratorDateAudio); + iteratorDatePhoto.setHours(iteratorDatePhoto.getHours() + 2); + iteratorDateVideo.setHours(iteratorDateVideo.getHours() + 2); + iteratorDateCutting.setHours(iteratorDateCutting.getHours() + 2); + iteratorDateAudio.setHours(iteratorDateAudio.getHours() + 2); + } + + var labEntitiesPhoto = testDatesPhoto.map(function (date) { + return { + "date": date, + "duration": "120", + "location": "Amalienstraße 17", + "passed": false, + "labTypeId": labtypes[0].id, + "semesterId": scenarioSemester.id + }; + }); + var labEntitiesVideo = testDatesVideo.map(function (date) { + return { + "date": date, + "duration": "120", + "location": "Amalienstraße 17", + "passed": false, + "labTypeId": labtypes[1].id, + "semesterId": scenarioSemester.id + }; + }); + var labEntitiesAudio = testDatesAudio.map(function (date) { + return { + "date": date, + "duration": "120", + "location": "Amalienstraße 17", + "passed": false, + "labTypeId": labtypes[2].id, + "semesterId": scenarioSemester.id + }; + }); + var labEntitiesCutting = testDatesCutting.map(function (date) { + return { + "date": date, + "duration": "120", + "location": "Amalienstraße 17", + "passed": false, + "labTypeId": labtypes[3].id, + "semesterId": scenarioSemester.id + }; + }); + labEntitiesCutting = labEntitiesCutting.concat(labEntitiesCutting, + labEntitiesCutting, labEntitiesCutting, labEntitiesCutting, labEntitiesCutting); + var labEntities = labEntitiesPhoto.concat(labEntitiesVideo, labEntitiesAudio, labEntitiesCutting); + + app.models.Lab.create(labEntities, function (err, documents) { + + if (err) throw err; + labs = labs.concat(documents); + console.log(documents.length + ' Labs created'); + + var labIds = labs.map(function (lab) { + return lab.id; + }); + + // Randomly choose tutorPossibleLabs + var tutors = [platformUsers[5], platformUsers[6], platformUsers[7], platformUsers[8]]; + var photoLabIds = labIds.slice(1, 21); + var videoLabIds = labIds.slice(21, 41); + var audioLabIds = labIds.slice(41, 61); + tutors.forEach(function (tutor) { + var possibleLabIdsPhoto = randomSubset(photoLabIds); + var possibleLabIdsVideo = randomSubset(videoLabIds); + var possibleLabIdsAudio = randomSubset(audioLabIds); + var possibleLabIds = possibleLabIdsPhoto.concat(possibleLabIdsVideo, possibleLabIdsAudio); + app.models.PlatformUser.updateAll( + {"id": tutor.id}, + {"tutorPossibleLabs": possibleLabIds}, + function (err) { + if (err) throw err; + // Let's be asynch again + } + ); + }); + + console.log('Random tutorPossibleLabs assigned'); + + // Generate students + var pendingStudentObjects = generateStudents(48, scenarioSemester.id); + app.models.PendingPlatformUser.create(pendingStudentObjects, function (err) { + + if (err) throw err; + + var studentObjects = pendingStudentObjects.map(function (pendingObject) { + var updatedEntry = pendingObject; + updatedEntry.password = 'test'; + return updatedEntry; + }); + app.models.PlatformUser.create(studentObjects, function (err, createdStudents) { + + if (err) throw err; + scenarioStudents = createdStudents; + console.log(scenarioStudents.length + ' PlatformUsers created'); + + // Generate groups + var numberOfGroups = Math.floor(scenarioStudents.length / 4); + var groupMemberIds = [], groupIndex = 0; + while (groupIndex < numberOfGroups) { + var firstStudentIndex = groupIndex * 4; + groupMemberIds[groupIndex] = [ + scenarioStudents[firstStudentIndex].id, + scenarioStudents[firstStudentIndex + 1].id, + scenarioStudents[firstStudentIndex + 2].id, + scenarioStudents[firstStudentIndex + 3].id + ]; + groupIndex++; + } + var groupObjects = groupMemberIds.map(function (memberList) { + return { + "name": randomName(true), + "groupMemberIds": memberList, + "semesterId": scenarioSemester.id + }; + }); + app.models.Group.create(groupObjects, function (err, createdGroups) { + + if (err) throw err; + scenarioGroups = createdGroups; + console.log(scenarioGroups.length + ' Groups created'); + + // Generate priority mappings for all groups + var priorityObjects = []; + scenarioGroups.forEach(function (group) { + priorityObjects = priorityObjects.concat(generateRandomPriorities(group, photoLabIds, 2)); + priorityObjects = priorityObjects.concat(generateRandomPriorities(group, videoLabIds, 2)); + priorityObjects = priorityObjects.concat(generateRandomPriorities(group, audioLabIds, 2)); + }); + + + // Link Priority Objects to Groups. + var index = 0; + for (var i = 0; i < priorityObjects.length; i++) { + + // This is needed to avoid a weird race-condition (Lost Update), which happens when the Priority Objects are + // seeded too fast. + setTimeout(function () { + app.models.Priority.create(priorityObjects[index], function (err, created) { + if (err) { + console.log(priorityObjects[index]); + throw err; + return; + } + if(index == priorityObjects.length-1){ + console.log((index+1)+' Priorities created'); + console.log('\nSeeding of the DB finished'); + } + }); + index++; + + }, 100 * i); + + } + + }); + }); + }); + }); +}; + diff --git a/server/component-config.json b/server/component-config.json new file mode 100644 index 0000000..f36959a --- /dev/null +++ b/server/component-config.json @@ -0,0 +1,5 @@ +{ + "loopback-component-explorer": { + "mountPath": "/explorer" + } +} diff --git a/server/config.json b/server/config.json new file mode 100644 index 0000000..40d45f4 --- /dev/null +++ b/server/config.json @@ -0,0 +1,23 @@ +{ + "restApiRoot": "/api", + "host": "0.0.0.0", + "port": 3000, + "remoting": { + "context": false, + "rest": { + "normalizeHttpPath": false, + "xml": false + }, + "json": { + "strict": false, + "limit": "100kb" + }, + "urlencoded": { + "extended": true, + "limit": "100kb" + }, + "cors": false, + "handleErrors": false + }, + "legacyExplorer": false +} diff --git a/server/datasources.development.json b/server/datasources.development.json new file mode 100644 index 0000000..fb4d90a --- /dev/null +++ b/server/datasources.development.json @@ -0,0 +1,16 @@ +{ + "db": { + "name": "db", + "connector": "memory" + }, + "mongo": { + "host": "localhost", + "port": 27017, + "url": "", + "database": "mt-api", + "password": "", + "name": "mongo", + "user": "", + "connector": "mongodb" + } +} diff --git a/server/datasources.json b/server/datasources.json new file mode 100644 index 0000000..2566419 --- /dev/null +++ b/server/datasources.json @@ -0,0 +1,32 @@ +{ + "db": { + "name": "db", + "connector": "memory" + }, + "mongo": { + "host": "ds145868.mlab.com", + "port": 45868, + "url": "", + "database": "mt-api", + "password": "l00pback", + "name": "mongo", + "user": "mt-loopback", + "connector": "mongodb" + }, + "Email": { + "name": "Email", + "connector": "mail", + "transports": [ + { + "type": "SMTP", + "host": "smtp.gmail.com", + "secure": true, + "port": 465, + "auth": { + "user": "mediatechnologytest2017@gmail.com", + "pass": "mtadmin2017" + } + } + ] + } +} diff --git a/server/datasources.staging.json b/server/datasources.staging.json new file mode 100644 index 0000000..ef4b8ea --- /dev/null +++ b/server/datasources.staging.json @@ -0,0 +1,16 @@ +{ + "db": { + "name": "db", + "connector": "memory" + }, + "mongo": { + "host": "ds117189.mlab.com", + "port": 17189, + "url": "", + "database": "mtplanr", + "password": "l00pback", + "name": "mongo", + "user": "mt-loopback", + "connector": "mongodb" + } +} diff --git a/server/middleware.development.json b/server/middleware.development.json new file mode 100644 index 0000000..071c11a --- /dev/null +++ b/server/middleware.development.json @@ -0,0 +1,10 @@ +{ + "final:after": { + "strong-error-handler": { + "params": { + "debug": true, + "log": true + } + } + } +} diff --git a/server/middleware.json b/server/middleware.json new file mode 100644 index 0000000..d4c1551 --- /dev/null +++ b/server/middleware.json @@ -0,0 +1,54 @@ +{ + "initial:before": { + "loopback#favicon": {} + }, + "initial": { + "compression": {}, + "cors": { + "params": { + "origin": true, + "credentials": true, + "maxAge": 86400 + } + }, + "helmet#xssFilter": {}, + "helmet#frameguard": { + "params": [ + "deny" + ] + }, + "helmet#hsts": { + "params": { + "maxAge": 0, + "includeSubdomains": true + } + }, + "helmet#hidePoweredBy": {}, + "helmet#ieNoOpen": {}, + "helmet#noSniff": {}, + "helmet#noCache": { + "enabled": false + } + }, + "session": {}, + "auth": {}, + "parse": {}, + "routes": { + "loopback#rest": { + "paths": [ + "${restApiRoot}" + ] + } + }, + "files": { + "loopback#static": { + "params": "$!../frontend/build/unbundled" + } + }, + "final": { + "loopback#urlNotFound": {} + }, + "final:after": { + "strong-error-handler": {} + } +} diff --git a/server/middleware.staging.json b/server/middleware.staging.json new file mode 100644 index 0000000..cde21fe --- /dev/null +++ b/server/middleware.staging.json @@ -0,0 +1,50 @@ +{ + "initial:before": { + "loopback#favicon": {} + }, + "initial": { + "compression": {}, + "cors": { + "params": { + "origin": true, + "credentials": true, + "maxAge": 86400 + } + }, + "helmet#xssFilter": {}, + "helmet#frameguard": { + "params": [ + "deny" + ] + }, + "helmet#hsts": { + "params": { + "maxAge": 0, + "includeSubdomains": true + } + }, + "helmet#hidePoweredBy": {}, + "helmet#ieNoOpen": {}, + "helmet#noSniff": {}, + "helmet#noCache": { + "enabled": false + } + }, + "session": {}, + "auth": {}, + "parse": {}, + "routes": { + "loopback#rest": { + "paths": [ + "${restApiRoot}" + ] + } + }, + "files": {}, + "final": { + "loopback#urlNotFound": {} + }, + "final:after": { + "strong-error-handler": {} + } +} diff --git a/server/model-config.json b/server/model-config.json new file mode 100644 index 0000000..9f6e1de --- /dev/null +++ b/server/model-config.json @@ -0,0 +1,81 @@ +{ + "_meta": { + "sources": [ + "loopback/common/models", + "loopback/server/models", + "../common/models", + "./models" + ], + "mixins": [ + "loopback/common/mixins", + "loopback/server/mixins", + "../common/mixins", + "./mixins" + ] + }, + "User": { + "dataSource": "db" + }, + "AccessToken": { + "dataSource": "mongo", + "public": false + }, + "ACL": { + "dataSource": "mongo", + "public": false + }, + "RoleMapping": { + "dataSource": "mongo", + "public": false + }, + "Role": { + "dataSource": "mongo", + "public": false + }, + "PlatformUser": { + "dataSource": "mongo", + "public": true, + "options": { + "emailVerificationRequired": true + } + }, + "Semester": { + "dataSource": "mongo", + "public": true + }, + "UniqueDate": { + "dataSource": "mongo", + "public": true + }, + "Exercise": { + "dataSource": "mongo", + "public": true + }, + "Group": { + "dataSource": "mongo", + "public": true + }, + "Lab": { + "dataSource": "mongo", + "public": true + }, + "Priority": { + "dataSource": "mongo", + "public": true + }, + "LabType": { + "dataSource": "mongo", + "public": true + }, + "PendingPlatformUser": { + "dataSource": "mongo", + "public": true + }, + "NewsEntry": { + "dataSource": "mongo", + "public": true + }, + "Email": { + "dataSource": "Email" + } +} diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..52a8e81 --- /dev/null +++ b/server/server.js @@ -0,0 +1,35 @@ +'use strict'; + +var loopback = require('loopback'); +var boot = require('loopback-boot'); + +var app = module.exports = loopback(); + +app.start = function() { + // start the web server + return app.listen(function() { + app.emit('started'); + var baseUrl = app.get('url').replace(/\/$/, ''); + console.log('Web server listening at: %s', baseUrl); + if (app.get('loopback-component-explorer')) { + var explorerPath = app.get('loopback-component-explorer').mountPath; + console.log('Browse your REST API at %s%s', baseUrl, explorerPath); + } + }); +}; + +// Bootstrap the application, configure models, datasources and middleware. +// Sub-apps like REST API are mounted via boot scripts. +boot(app, __dirname, function(err) { + if (err) throw err; + + // start the server if `$ node server.js` + if (require.main === module) + app.start(); +}); + +// Because of this: https://github.com/strongloop/loopback-connector-mongodb/issues/128 +var ObjectID = app.models.RoleMapping.getDataSource().connector.getDefaultIdType(); +app.models.RoleMapping.defineProperty('principalId', { + type: ObjectID, +}); diff --git a/views/error.jade b/views/error.jade deleted file mode 100644 index 51ec12c..0000000 --- a/views/error.jade +++ /dev/null @@ -1,6 +0,0 @@ -extends layout - -block content - h1= message - h2= error.status - pre #{error.stack} diff --git a/views/index.jade b/views/index.jade deleted file mode 100644 index 3d63b9a..0000000 --- a/views/index.jade +++ /dev/null @@ -1,5 +0,0 @@ -extends layout - -block content - h1= title - p Welcome to #{title} diff --git a/views/layout.jade b/views/layout.jade deleted file mode 100644 index 15af079..0000000 --- a/views/layout.jade +++ /dev/null @@ -1,7 +0,0 @@ -doctype html -html - head - title= title - link(rel='stylesheet', href='/stylesheets/style.css') - body - block content