_ = require 'lodash' utils = require '../lib/utils' errors = require '../commons/errors' schemas = require '../../app/schemas/schemas' wrap = require 'co-express' log = require 'winston' Promise = require 'bluebird' database = require '../commons/database' mongoose = require 'mongoose' Classroom = require '../models/Classroom' Course = require '../models/Course' Campaign = require '../models/Campaign' Level = require '../models/Level' parse = require '../commons/parse' LevelSession = require '../models/LevelSession' User = require '../models/User' CourseInstance = require '../models/CourseInstance' TrialRequest = require '../models/TrialRequest' sendwithus = require '../sendwithus' co = require 'co' module.exports = fetchByCode: wrap (req, res, next) -> code = req.query.code return next() unless req.query.hasOwnProperty('code') classroom = yield Classroom.findOne({ code: code.toLowerCase().replace(RegExp(' ', 'g') , '') }).select('name ownerID aceConfig') if not classroom log.debug("classrooms.fetchByCode: Couldn't find Classroom with code: #{code}") throw new errors.NotFound('Classroom not found.') classroom = classroom.toObject() # Tack on the teacher's name for display to the user owner = (yield User.findOne({ _id: mongoose.Types.ObjectId(classroom.ownerID) }).select('name')).toObject() res.status(200).send({ data: classroom, owner } ) getByOwner: wrap (req, res, next) -> options = req.query ownerID = options.ownerID return next() unless ownerID throw new errors.UnprocessableEntity('Bad ownerID') unless utils.isID ownerID throw new errors.Unauthorized() unless req.user unless req.user.isAdmin() or ownerID is req.user.id log.debug("classrooms.getByOwner: Can't fetch classroom you don't own. User: #{req.user.id} Owner: #{ownerID}") throw new errors.Forbidden('"ownerID" must be yourself') sanitizedOptions = {} unless _.isUndefined(options.archived) # Handles when .archived is true, vs false-or-null sanitizedOptions.archived = { $ne: not (options.archived is 'true') } dbq = Classroom.find _.merge sanitizedOptions, { ownerID: mongoose.Types.ObjectId(ownerID) } dbq.select(parse.getProjectFromReq(req)) classrooms = yield dbq classrooms = (classroom.toObject({req: req}) for classroom in classrooms) res.status(200).send(classrooms) fetchAllLevels: wrap (req, res, next) -> classroom = yield database.getDocFromHandle(req, Classroom) if not classroom throw new errors.NotFound('Classroom not found.') levelOriginals = [] for course in classroom.get('courses') or [] for level in course.levels levelOriginals.push(level.original) query = {$and: [ {original: { $in: levelOriginals }} {$or: [{primerLanguage: {$exists: false}}, {primerLanguage: { $ne: classroom.get('aceConfig')?.language }}]} {slug: { $exists: true }} ]} levels = yield Level.find(query).select(parse.getProjectFromReq(req)) levels = (level.toObject({ req: req }) for level in levels) # maintain course order levelMap = {} for level in levels levelMap[level.original] = level levels = (levelMap[levelOriginal.toString()] for levelOriginal in levelOriginals) res.status(200).send(_.filter(levels)) # for dev server where not all levels will be found fetchLevelsForCourse: wrap (req, res) -> classroom = yield database.getDocFromHandle(req, Classroom) if not classroom throw new errors.NotFound('Classroom not found.') levelOriginals = [] for course in classroom.get('courses') or [] if course._id.toString() isnt req.params.courseID continue for level in course.levels levelOriginals.push(level.original) query = {$and: [ {original: { $in: levelOriginals }} {$or: [{primerLanguage: {$exists: false}}, {primerLanguage: { $ne: classroom.get('aceConfig')?.language }}]} {slug: { $exists: true }} ]} levels = yield Level.find(query).select(parse.getProjectFromReq(req)) levels = (level.toObject({ req: req }) for level in levels) # maintain course order levelMap = {} for level in levels levelMap[level.original] = level levels = (levelMap[levelOriginal.toString()] for levelOriginal in levelOriginals when levelMap[levelOriginal.toString()]) res.status(200).send(levels) fetchMemberSessions: wrap (req, res, next) -> # Return member sessions for assigned courses throw new errors.Unauthorized() unless req.user classroom = yield database.getDocFromHandle(req, Classroom) throw new errors.NotFound('Classroom not found.') if not classroom throw new errors.Forbidden('You do not own this classroom.') unless req.user.isAdmin() or classroom.get('ownerID').equals(req.user._id) courseLevelsMap = {} for course in classroom.get('courses') ? [] # TODO: is LevelSession.level.original really a string in practice, instead of ObjectId set in schema? # https://github.com/codecombat/codecombat/blob/master/server/middleware/levels.coffee#L18 courseLevelsMap[course._id.toHexString()] = _.map(course.levels, (l) -> l.original?.toHexString()) courseInstances = yield CourseInstance.find({classroomID: classroom._id}).select('_id courseID members').lean() memberCoursesMap = {} for courseInstance in courseInstances for userID in courseInstance.members ? [] memberCoursesMap[userID.toHexString()] ?= [] memberCoursesMap[userID.toHexString()].push(courseInstance.courseID) memberLimit = parse.getLimitFromReq(req, {default: 10, max: 100, param: 'memberLimit'}) memberSkip = parse.getSkipFromReq(req, {param: 'memberSkip'}) members = classroom.get('members') or [] members = members.slice(memberSkip, memberSkip + memberLimit) dbqs = [] select = 'state.complete level creator playtime changed created dateFirstCompleted submitted' for member in members levelOriginals = [] for courseID in memberCoursesMap[member.toHexString()] ? [] levelOriginals = levelOriginals.concat(courseLevelsMap[courseID.toHexString()] ? []) query = {creator: member.toHexString(), 'level.original': {$in: levelOriginals}} dbqs.push(LevelSession.find(query).select(select).lean().exec()) results = yield dbqs sessions = _.flatten(results) res.status(200).send(sessions) fetchMembers: wrap (req, res, next) -> throw new errors.Unauthorized() unless req.user memberLimit = parse.getLimitFromReq(req, {default: 10, max: 100, param: 'memberLimit'}) memberSkip = parse.getSkipFromReq(req, {param: 'memberSkip'}) classroom = yield database.getDocFromHandle(req, Classroom) throw new errors.NotFound('Classroom not found.') if not classroom isOwner = classroom.get('ownerID').equals(req.user._id) isMember = req.user.id in (m.toString() for m in classroom.get('members')) unless req.user.isAdmin() or isOwner or isMember log.debug "classrooms.fetchMembers: Can't fetch members for class (#{classroom.id}) you (#{req.user.id}) don't own and aren't a member of." throw new errors.Forbidden('You do not own this classroom.') memberIDs = classroom.get('members') or [] memberIDs = memberIDs.slice(memberSkip, memberSkip + memberLimit) members = yield User.find({ _id: { $in: memberIDs }}).select(parse.getProjectFromReq(req)) # members = yield User.find({ _id: { $in: memberIDs }, deleted: { $ne: true }}).select(parse.getProjectFromReq(req)) memberObjects = (member.toObject({ req: req, includedPrivates: ["name", "email"] }) for member in members) res.status(200).send(memberObjects) post: wrap (req, res) -> throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous() unless req.user?.isTeacher() log.debug "classrooms.post: Can't create classroom if you (#{req.user?.id}) aren't a teacher." throw new errors.Forbidden() classroom = database.initDoc(req, Classroom) classroom.set 'ownerID', req.user._id classroom.set 'members', [] database.assignBody(req, classroom) # Copy over data from how courses are right now coursesData = yield module.exports.generateCoursesData(classroom.get('aceConfig')?.language, req.user?.isAdmin()) classroom.set('courses', coursesData) # finish database.validateDoc(classroom) classroom = yield classroom.save() res.status(201).send(classroom.toObject({req: req})) updateCourses: wrap (req, res) -> throw new errors.Unauthorized() unless req.user and not req.user.isAnonymous() classroom = yield database.getDocFromHandle(req, Classroom) if not classroom throw new errors.NotFound('Classroom not found.') unless req.user._id.equals(classroom.get('ownerID')) or req.user.isAdmin() throw new errors.Forbidden('Only the owner may update their classroom content') # make sure updates are based on owner, not logged in user if not req.user._id.equals(classroom.get('ownerID')) owner = yield User.findById(classroom.get('ownerID')) else owner = req.user coursesData = yield module.exports.generateCoursesData(classroom.get('aceConfig')?.language, owner.isAdmin()) classroom.set('courses', coursesData) classroom = yield classroom.save() res.status(200).send(classroom.toObject({req: req})) generateCoursesData: co.wrap (classLanguage, isAdmin) -> # helper function for generating the latest version of courses query = {} query = {releasePhase: 'released'} unless isAdmin courses = yield Course.find(query) courses = Course.sortCourses courses campaigns = yield Campaign.find({_id: {$in: (course.get('campaignID') for course in courses)}}) campaignMap = {} campaignMap[campaign.id] = campaign for campaign in campaigns coursesData = [] for course in courses courseData = { _id: course._id, levels: [] } campaign = campaignMap[course.get('campaignID').toString()] levels = _.values(campaign.get('levels')) levels = _.sortBy(levels, 'campaignIndex') for level in levels continue if classLanguage and level.primerLanguage is classLanguage levelData = { original: mongoose.Types.ObjectId(level.original) } _.extend(levelData, _.pick(level, 'type', 'slug', 'name', 'practice', 'practiceThresholdMinutes', 'primerLanguage', 'shareable')) courseData.levels.push(levelData) coursesData.push(courseData) return coursesData join: wrap (req, res) -> unless req.body?.code throw new errors.UnprocessableEntity('Need a code') if req.user.isTeacher() log.debug("classrooms.join: Cannot join a classroom as a teacher: #{req.user.id}") throw new errors.Forbidden('Cannot join a classroom as a teacher') code = req.body.code.toLowerCase().replace(RegExp(' ', 'g'), '') classroom = yield Classroom.findOne({code: code}) if not classroom log.debug("classrooms.join: Classroom not found with code #{code}") throw new errors.NotFound("Classroom not found with code #{code}") members = _.clone(classroom.get('members')) if _.any(members, (memberID) -> memberID.equals(req.user._id)) return res.send(classroom.toObject({req: req})) update = { $push: { members : req.user._id }} yield classroom.update(update) members.push req.user._id classroom.set('members', members) # make user role student if not req.user.get('role') req.user.set('role', 'student') yield req.user.save() # join any course instances for free courses in the classroom courseIDs = (course._id for course in classroom.get('courses')) courses = yield Course.find({_id: {$in: courseIDs}, free: true}) freeCourseIDs = (course._id for course in courses) freeCourseInstances = yield CourseInstance.find({ classroomID: classroom._id, courseID: {$in: freeCourseIDs} }).select('_id') freeCourseInstanceIDs = (courseInstance._id for courseInstance in freeCourseInstances) yield CourseInstance.update({_id: {$in: freeCourseInstanceIDs}}, { $addToSet: { members: req.user._id }}) yield User.update({ _id: req.user._id }, { $addToSet: { courseInstances: { $each: freeCourseInstanceIDs } } }) res.send(classroom.toObject({req: req})) setStudentPassword: wrap (req, res, next) -> newPassword = req.body.password { classroomID, memberID } = req.params teacherID = req.user.id return next() if teacherID is memberID or not newPassword ownedClassrooms = yield Classroom.find({ ownerID: mongoose.Types.ObjectId(teacherID) }) ownedStudentIDs = _.flatten ownedClassrooms.map (c) -> c.get('members').map (id) -> id.toString() unless memberID in ownedStudentIDs throw new errors.Forbidden("Can't reset the password of a student that's not in one of your classrooms.") student = yield User.findById(memberID) if student.get('emailVerified') log.debug "classrooms.setStudentPassword: Can't reset password for a student (#{memberID}) that has verified their email address." throw new errors.Forbidden("Can't reset password for a student that has verified their email address.") { valid, error } = tv4.validateResult(newPassword, schemas.passwordString) unless valid throw new errors.UnprocessableEntity(error.message) yield student.update({ $set: { passwordHash: User.hashPassword(newPassword) } }) res.status(200).send({}) inviteMembers: wrap (req, res) -> if not req.body.emails log.debug "classrooms.inviteMembers: No emails included in request: #{JSON.stringify(req.body)}" throw new errors.UnprocessableEntity('Emails not included') classroom = yield database.getDocFromHandle(req, Classroom) if not classroom throw new errors.NotFound('Classroom not found.') unless classroom.get('ownerID').equals(req.user?._id) log.debug "classroom_handler.inviteMembers: Can't invite to classroom (#{classroom.id}) you (#{req.user.get('_id')}) don't own" throw new errors.Forbidden('Must be owner of classroom to send invites.') for email in req.body.emails joinCode = (classroom.get('codeCamel') or classroom.get('code')) context = email_id: sendwithus.templates.course_invite_email recipient: address: email email_data: teacher_name: req.user.broadName() class_name: classroom.get('name') join_link: "https://codecombat.com/students?_cc=" + joinCode join_code: joinCode sendwithus.api.send context, _.noop res.status(200).send({}) getUsers: wrap (req, res, next) -> throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin() classrooms = yield Classroom.find().select('ownerID members').lean() res.status(200).send(classrooms)