e-ucm/rage-analytics-backend

View on GitHub
lib/classes.js

Summary

Maintainability
F
1 wk
Test Coverage
'use strict';

module.exports = (function () {
    var Q = require('q');
    var Collection = require('easy-collections');
    var async = require('async');
    var db = require('./db');
    var utils = require('./utils');
    var classes = new Collection(db, 'classes');
    var courses = new Collection(db, 'courses');
    var groups = require('./groups');
    var groupings = require('./groupings');
    var activityCollection = new Collection(db, 'activities');

    var Validator = require('jsonschema').Validator;
    var v = new Validator();

    var classSchema = {
        id: '/Class',
        type: 'object',
        properties: {
            name: { type: 'string'},
            created: { type: 'date'},
            courseId: { anyOf: [
                {
                    type: 'ID'
                },
                {
                    type: 'null'
                }]
            },
            groups: {
                type: 'array',
                items: {type: 'ID'}
            },
            groupings: {
                type: 'array',
                items: {type: 'ID'}
            },
            participants: {
                type: 'object',
                properties: {
                    students: {
                        type: 'array',
                        required: true,
                        items: {type: 'string'}
                    },
                    assistants: {
                        type: 'array',
                        required: true,
                        items: {type: 'string'}
                    },
                    teachers: {
                        type: 'array',
                        required: true,
                        items: {type: 'string'}
                    }
                }
            },
            externalId: {
                type: 'array',
                required: false,
                items: {
                    type: 'object',
                    properties: {
                        domain: {
                            type: 'string',
                            required: true
                        },
                        id: {
                            type: 'string',
                            required: true
                        }
                    }
                }
            }
        },
        required: ['name', 'created', 'groups', 'groupings'],
        additionalProperties: false
    };
    v.addSchema(classSchema, '/ClassSchema');

    var classSchemaPut = {
        id: '/ClassPut',
        type: 'object',
        properties: {
            name: { type: 'string'},
            courseId: { anyOf: [
                {
                    type: 'ID'
                },
                {
                    type: 'null'
                }]
            },
            groups: {
                type: 'array',
                items: {type: 'ID'}
            },
            groupings: {
                type: 'array',
                items: {type: 'ID'}
            },
            participants: {
                type: 'object',
                properties: {
                    students: { anyOf: [
                        {
                            type: 'array',
                            items: {type: 'string'}
                        },
                        {
                            type: 'string'
                        }
                    ]},
                    assistants: { anyOf: [
                        {
                            type: 'array',
                            items: {type: 'string'}
                        },
                        {
                            type: 'string'
                        }
                    ]},
                    teachers: { anyOf: [
                        {
                            type: 'array',
                            items: {type: 'string'}
                        },
                        {
                            type: 'string'
                        }
                    ]}
                }
            },
            externalId: {
                type: 'array',
                items: {
                    type: 'object',
                    properties: {
                        domain: {
                            type: 'string',
                            required: true
                        },
                        id: {
                            type: 'string',
                            required: true
                        }
                    }
                }
            }
        },
        additionalProperties: false,
        minProperties: 1
    };
    v.addSchema(classSchemaPut, '/ClassPut');

    classes.sort = {
        _id: -1
    };



    var minTypes = {
        post: {
            '/classes/': {
                type: 'teacher',
                error: 'Not authorized create a new class'
            },
            '/activities/': {
                type: 'teacher',
                error: 'Not authorized create a new activity for this class'
            },
            '/activities/bundle': {
                type: 'teacher',
                error: 'Not authorized create a new activity for this class'
            },
            '/classes/:classId/groups': {
                type: 'teacher',
                error: 'Not authorized create a new group for this class'
            },
            '/classes/:classId/groupings': {
                type: 'teacher',
                error: 'Not authorized create a new grouping for this class'
            }
        },
        get: {
            '/classes/': {
                type: 'admin',
                error: 'Not authorized to get all the classes'
            },
            '/classes/my': {
                type: 'student',
                error: 'Not authorized to get your classes'
            },
            '/classes/external/:domain/:externalId': {
                type: 'student',
                error: 'Not authorized to get this class'
            },
            '/classes/:classId': {
                type: 'student',
                error: 'Not authorized to get this class'
            },
            '/classes/:classId/activities': {
                type: 'assistant',
                error: 'Not authorized to get all the activities for the class'
            },
            '/classes/:classId/activities/my': {
                type: 'student',
                error: 'Not authorized to get your activities for this class'
            },
            '/classes/:classId/groups': {
                type: 'student',
                error: 'Not authorized to get all the groups for this class'
            },
            '/classes/:classId/groupings': {
                type: 'student',
                error: 'Not authorized to get all the groupings for this class'
            }
        },
        put: {
            '/classes/:classId': {
                type: 'teacher',
                error: 'Not authorized to modify this class'
            },
            '/classes/external/:domain/:externalId': {
                type: 'teacher',
                error: 'Not authorized to modify this class'
            },
            '/classes/:classId/remove': {
                type: 'teacher',
                error: 'Not authorized to modify (remove) this class'
            },
            '/classes/external/:domain/:externalId/remove': {
                type: 'teacher',
                error: 'Not authorized to modify (remove) this class'
            }
        },
        delete: {
            '/classes/:classId': {
                type: 'teacher',
                error: 'Not authorized to delete this class'
            },
            '/classes/external/:domain/:externalId': {
                type: 'teacher',
                error: 'Not authorized to delete this class'
            }
        }
    };

    classes.isAuthorizedFor = function(classId, username, method, call) {
        return isAuthorizedForCommon({ _id: classes.toObjectID(classId) }, username, method, call);
    };

    classes.isAuthorizedForExternal = function(domain, externalId, username, method, call) {
        var query = { externalId: { $elemMatch: { domain: domain, id: externalId } } };
        return isAuthorizedForCommon(query, username, method, call);
    };

    var isAuthorizedForCommon = function(query, username, method, call) {
        var deferred = Q.defer();

        if (!minTypes[method] || !minTypes[method][call] || !minTypes[method][call].type) {
            console.log('unauthorized');
            return 'unauthorized';
        }

        var minType = minTypes[method][call].type;
        var ignoreStudents = minType !== 'student';
        var ignoreAssistants = ignoreStudents && minType !== 'assistant';

        try {
            classes.find(query, true)
                .then(function (classReq) {
                    if (classReq) {
                        var typeComp = function (type) {
                            if (getBestType(minType, type) === type) {
                                deferred.resolve(classReq);
                            } else {
                                deferred.reject({
                                    status: 401,
                                    message: minTypes[method][call].error
                                });
                            }
                        };
                        if (classReq.groupings && classReq.groupings.length > 0) {
                            return groupings.getUserTypeForArray(classReq.groupings, username, ignoreStudents, ignoreAssistants).then(typeComp);
                        }
                        if (classReq.groups && classReq.groups.length > 0) {
                            return groups.getUserTypeForArray(classReq.groups, username, ignoreStudents, ignoreAssistants).then(typeComp);
                        }
                        typeComp(getUserType(classReq, username));
                    } else {
                        deferred.reject({
                            status: 404,
                            message: 'Class not found'
                        });
                    }
                })
            .fail(function(error) {
                deferred.reject(error);
            });
        }catch (exception) {
            deferred.reject(exception);
        }

        return deferred.promise;
    };

    function getBestType(type1, type2) {
        if (type1 === 'admin' || type2 === 'admin') {
            return 'admin';
        }
        if (type1 === 'teacher' || type2 === 'teacher') {
            return 'teacher';
        }
        if (type1 === 'assistant' || type2 === 'assistant') {
            return 'assistant';
        }
        if (type1 === 'student' || type2 === 'student') {
            return 'student';
        }
        return 'unauthorized';
    }

    /**
     * Returns the classes for gameId:versionId
     */
    classes.getClasses = function () {
        return classes.find();
    };
    var extractId = function (object) {
        return object._id;
    };

    var getUserClasses = function (user, otherfilters) {
        otherfilters = otherfilters || {};

        var deferred = Q.defer();
        async.parallel({
            g: function (callback) {
                var r = {groups: [], groupings: []};
                return groups.getUserGroups(user)
                    .then(function (groups) {
                        r.groups = groups.map(extractId);
                        if (r.groups.length > 0) {
                            return groupings.getGroupGroupings(r.groups)
                                .then(function (groupings) {
                                    r.groupings = groupings.map(extractId);
                                    return callback(null, r);
                                }).fail(callback);
                        }
                        return callback(null, r);
                    }).fail(callback);
            }
        }, function (err, results) {
            if (err) {
                return deferred.reject(err);
            }
            var query = {
                $or: [
                    {'participants.teachers': user, groups: {$size: 0}, groupings: {$size: 0}},
                    {'participants.students': user, groups: {$size: 0}, groupings: {$size: 0}},
                    {'participants.assistants': user, groups: {$size: 0}, groupings: {$size: 0}},
                    {groups: {$in: results.g.groups}, groupings: {$size: 0}},
                    {groupings: {$in: results.g.groupings}}
                ]
            };

            for (var i = 0; i < query.$or.length; i++) {
                query.$or[i] = Object.assign(query.$or[i], otherfilters);
            }

            classes.find(query).then(function (classes) {
                deferred.resolve(classes);
            }).fail(function (err) {
                deferred.reject(err);
            });
        });

        return deferred.promise;
    };

    /**
     * Returns the classes where a user participates
     */
    classes.getUserClasses = function (user) {
        return getUserClasses(user);
    };


    /**
     * Creates a new class for the given gameId:versionId.
     * @Returns a promise with the class created
     */
    classes.createClass = function (username, name) {
        var classObj = {
            name: name,
            created: new Date(),
            participants: {
                teachers: [username],
                students: [],
                assistants: []
            },
            groups: [],
            groupings: [],
            externalId: []
        };

        var validationObj = v.validate(classObj, classSchema);
        if (validationObj.errors && validationObj.errors.length > 0) {
            throw {
                message: 'Course bad format: ' + validationObj.errors[0],
                status: 400
            };
        } else {
            return classes.insert(classObj);
        }
    };

    classes.removeClass = function (classId, username) {
        return classes.findById(classId)
            .then(function (classRes) {
                if (!classRes) {
                    throw {
                        message: 'Class does not exist',
                        status: 400
                    };
                }

                if (!classRes.participants || !classRes.participants.teachers || classRes.participants.teachers.indexOf(username) === -1) {
                    throw {
                        message: 'You don\'t have permission to delete this class.',
                        status: 401
                    };
                }

                return classes.removeById(classId).then(function (result, err) {
                    if (!err) {
                        return {
                            message: 'Success.'
                        };
                    }
                });
            });
    };

    classes.modifyClass = function (classId, username, body, add) {
        var participants = {};
        if (!body.participants && body.students) {
            participants.students = body.students;
            delete body.students;
        }
        if (!body.participants && body.teachers) {
            participants.teachers = body.teachers;
            delete body.teachers;
        }
        if (!body.participants) {
            body.participants = participants;
        }
        var validationObj = v.validate(body, classSchemaPut);
        if (validationObj.errors && validationObj.errors.length > 0) {
            throw {
                message: 'Course bad format: ' + validationObj.errors[0],
                status: 400
            };
        } else {
            return classes.find(classes.toObjectID(classId), true)
                .then(function (classRes) {
                    if (!classRes) {
                        throw {
                            message: 'Class does not exist',
                            status: 400
                        };
                    }

                    if (!classRes.participants || !classRes.participants.teachers || classRes.participants.teachers.indexOf(username) === -1) {
                        throw {
                            message: 'You don\'t have permission to modify this class.',
                            status: 401
                        };
                    }

                    if (body._id) {
                        delete body._id;
                    }

                    if (body.versionId) {
                        delete body.versionId;
                    }

                    if (body.groups) {
                        body.groups = body.groups.map(classes.toObjectID);
                    }

                    if (body.groupings) {
                        body.groupings = body.groupings.map(classes.toObjectID);
                    }

                    var update = {};
                    utils.addToArrayHandler(update, body, 'participants.teachers', add);
                    utils.addToArrayHandler(update, body, 'participants.students', add);
                    utils.addToArrayHandler(update, body, 'participants.assistants', add);

                    utils.addToArrayHandler(update, body, 'groups', add);
                    utils.addToArrayHandler(update, body, 'groupings', add);

                    if (add) {
                        if (body.name && typeof body.name === 'string') {
                            if (!update.$set) {
                                update.$set = {};
                            }
                            update.$set.name = body.name;
                        }
                    }

                    if (body.externalId) {
                        if (!update.$set) {
                            update.$set = {};
                        }

                        update.$set.externalId = body.externalId;
                    }

                    if (body.courseId === null) {
                        if (!update.$set) {
                            update.$set = {};
                        }
                        update.$set.courseId = undefined;
                        return classes.findAndUpdate(classId, update);
                    }
                    if (body.courseId) {
                        return courses.findById(body.courseId)
                            .then(function (course) {
                                if (!course) {
                                    throw {
                                        message: 'Course does not exist',
                                        status: 400
                                    };
                                }
                                if (!update.$set) {
                                    update.$set = {};
                                }
                                update.$set.courseId = course._id;
                                return classes.findAndUpdate(classId, update);
                            });
                    }
                    return classes.findAndUpdate(classId, update);
                });
        }
    };

    var getUserType = function(classRes, username, ignoreStudents, ignoreAssistants, ignoreTeachers) {
        if (classRes) {
            if (!ignoreTeachers && classRes.participants.teachers.indexOf(username) !== -1) {
                return 'teacher';
            }
            if (!ignoreAssistants && classRes.participants.assistants.indexOf(username) !== -1) {
                return 'assistant';
            }
            if (!ignoreStudents && classRes.participants.students.indexOf(username) !== -1) {
                return 'student';
            }
        }
        return 'unauthorized';
    };

    classes.getUserType = function(classId, username, ignoreStudents, ignoreAssistants, ignoreTeachers) {
        return classes.findById(classId).then(function (classRes) {
            return getUserType(classRes, username, ignoreStudents, ignoreAssistants, ignoreTeachers);
        });
    };

    classes.preRemove(function(_id, next) {
        activityCollection.remove({
            classesId: _id
        }).then(function () {
            next();
        }).fail(function() {
            next();
        });
    });

    return classes;
})();