mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-24 16:17:57 -05:00
8496343a02
This adds the ability to verify email addresses of a user, so we know they have access to the email address on their account. Until a user has verified their email address, any teacher of a class they're in can reset their password for them via the Teacher Dashboard. When a user's email address is verified, a teacher may trigger a password recovery email to be sent to the student. Verification links are valid forever, until the user changes the email address they have on file. They are created using a timestamp, with a sha256 of timestamp+salt+userID+email. Currently the hash value is rather long, could be shorter. Squashed commit messages: Add server endpoints for verifying email address Add server endpoints for verifying email address (pt 2) Add Server+Client endpoint for sending verification email Add client view for verification links Add Edit Student Modal for resetting passwords Add specs for EditStudentModal Tweak method name in EditStudentModal Add edit student button to TeacherClassView Fix up frontend for teacher password resetting Add middleware for teacher password resetting Improve button UX in EditStudentModal Add JoinClassModal Add welcome emails, use broad name Use email without domain as fallback instead of full email Fetch user on edit student modal open Don't allow password reset if student email is verified Set role to student on user signup with classCode Tweak interface for joinClassModal Add button to request verification email for yourself Fix verify email template ID Move text to en.coffee Minor tweaks Fix code review comments Fix some tests, disable a broken one Fix misc tests Fix more tests Refactor recovery email sending to auth Fix overbroad sass Add options to refactored recovery email function Rename getByCode to fetchByCode Fix error message Fix up error handling in users middleware Use .get instead of .toObject Use findById Fix more code review comments Disable still-broken test
976 lines
44 KiB
CoffeeScript
976 lines
44 KiB
CoffeeScript
schema = require '../../app/schemas/models/user'
|
|
crypto = require 'crypto'
|
|
request = require 'request'
|
|
User = require './../models/User'
|
|
Handler = require '../commons/Handler'
|
|
mongoose = require 'mongoose'
|
|
config = require '../../server_config'
|
|
errors = require '../commons/errors'
|
|
async = require 'async'
|
|
log = require 'winston'
|
|
moment = require 'moment'
|
|
AnalyticsLogEvent = require '../models/AnalyticsLogEvent'
|
|
Clan = require '../models/Clan'
|
|
CourseInstance = require '../models/CourseInstance'
|
|
LevelSession = require '../models/LevelSession'
|
|
LevelSessionHandler = require './level_session_handler'
|
|
Payment = require '../models/Payment'
|
|
SubscriptionHandler = require './subscription_handler'
|
|
DiscountHandler = require './discount_handler'
|
|
EarnedAchievement = require '../models/EarnedAchievement'
|
|
UserRemark = require './../models/UserRemark'
|
|
{findStripeSubscription} = require '../lib/utils'
|
|
{isID} = require '../lib/utils'
|
|
slack = require '../slack'
|
|
sendwithus = require '../sendwithus'
|
|
Prepaid = require '../models/Prepaid'
|
|
UserPollsRecord = require '../models/UserPollsRecord'
|
|
EarnedAchievement = require '../models/EarnedAchievement'
|
|
|
|
serverProperties = ['passwordHash', 'emailLower', 'nameLower', 'passwordReset', 'lastIP']
|
|
candidateProperties = [
|
|
'jobProfile', 'jobProfileApproved', 'jobProfileNotes'
|
|
]
|
|
|
|
UserHandler = class UserHandler extends Handler
|
|
modelClass: User
|
|
|
|
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
|
|
|
getEditableProperties: (req, document) ->
|
|
props = super req, document
|
|
props.push 'permissions' unless config.isProduction or global.testing
|
|
props.push 'jobProfileApproved', 'jobProfileNotes','jobProfileApprovedDate' if req.user.isAdmin() # Admins naturally edit these
|
|
props.push @privateProperties... if req.user.isAdmin() # Admins are mad with power
|
|
if not req.user.isAdmin()
|
|
if document.isTeacher() and req.body.role not in User.teacherRoles
|
|
props = _.without props, 'role'
|
|
props
|
|
|
|
formatEntity: (req, document, publicOnly=false) =>
|
|
# TODO: Delete. This function is duplicated in server User model toObject transform.
|
|
return null unless document?
|
|
obj = document.toObject()
|
|
delete obj[prop] for prop in User.serverProperties
|
|
includePrivates = not publicOnly and (req.user and (req.user.isAdmin() or req.user._id.equals(document._id) or req.session.amActually is document.id))
|
|
delete obj[prop] for prop in User.privateProperties unless includePrivates
|
|
includeCandidate = not publicOnly and (includePrivates or (obj.jobProfile?.active and req.user and ('employer' in (req.user.get('permissions') ? [])) and @employerCanViewCandidate req.user, obj))
|
|
delete obj[prop] for prop in User.candidateProperties unless includeCandidate
|
|
return obj
|
|
|
|
waterfallFunctions: [
|
|
# FB access token checking
|
|
# Check the email is the same as FB reports
|
|
(req, user, callback) ->
|
|
fbID = req.query.facebookID
|
|
fbAT = req.query.facebookAccessToken
|
|
return callback(null, req, user) unless fbID and fbAT
|
|
url = "https://graph.facebook.com/me?access_token=#{fbAT}"
|
|
request(url, (err, response, body) ->
|
|
log.warn "Error grabbing FB token: #{err}" if err
|
|
body = JSON.parse(body)
|
|
emailsMatch = req.body.email is body.email
|
|
return callback(res: 'Invalid Facebook Access Token.', code: 422) unless emailsMatch
|
|
callback(null, req, user)
|
|
)
|
|
|
|
# GPlus access token checking
|
|
(req, user, callback) ->
|
|
gpID = req.query.gplusID
|
|
gpAT = req.query.gplusAccessToken
|
|
return callback(null, req, user) unless gpID and gpAT
|
|
url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=#{gpAT}"
|
|
request(url, (err, response, body) ->
|
|
log.warn "Error grabbing G+ token: #{err}" if err
|
|
body = JSON.parse(body)
|
|
emailsMatch = req.body.email is body.email
|
|
return callback(res: 'Invalid G+ Access Token.', code: 422) unless emailsMatch
|
|
callback(null, req, user)
|
|
)
|
|
|
|
# Email setting
|
|
(req, user, callback) ->
|
|
return callback(null, req, user) unless req.body.email?
|
|
emailLower = req.body.email.toLowerCase()
|
|
return callback(null, req, user) if emailLower is user.get('emailLower')
|
|
User.findOne({emailLower: emailLower}).exec (err, otherUser) ->
|
|
log.error "Database error setting user email: #{err}" if err
|
|
return callback(res: 'Database error.', code: 500) if err
|
|
|
|
if (req.query.gplusID or req.query.facebookID) and otherUser
|
|
# special case, log in as that user
|
|
return req.logIn(otherUser, (err) ->
|
|
return callback(res: 'Facebook user login error.', code: 500) if err
|
|
return callback(null, req, otherUser)
|
|
)
|
|
r = {message: 'is already used by another account', property: 'email'}
|
|
return callback({res: r, code: 409}) if otherUser
|
|
user.set('email', req.body.email)
|
|
callback(null, req, user)
|
|
|
|
# Name setting
|
|
(req, user, callback) ->
|
|
return callback(null, req, user) unless req.body.name
|
|
nameLower = req.body.name?.toLowerCase()
|
|
return callback(null, req, user) unless nameLower
|
|
return callback(null, req, user) if user.get 'anonymous' # anonymous users can have any name
|
|
return callback(null, req, user) if nameLower is user.get('nameLower')
|
|
User.findOne({nameLower: nameLower, anonymous: false}).exec (err, otherUser) ->
|
|
log.error "Database error setting user name: #{err}" if err
|
|
return callback(res: 'Database error.', code: 500) if err
|
|
r = {message: 'is already used by another account', property: 'name'}
|
|
console.log 'Another user exists' if otherUser
|
|
return callback({res: r, code: 409}) if otherUser
|
|
user.set('name', req.body.name)
|
|
callback(null, req, user)
|
|
|
|
# Subscription setting
|
|
(req, user, callback) ->
|
|
# TODO: Make subscribe vs. unsubscribe explicit. This property dance is confusing.
|
|
return callback(null, req, user) unless req.headers['x-change-plan'] # ensure only saves that are targeted at changing the subscription actually affect the subscription
|
|
return callback(null, req, user) unless req.body.stripe
|
|
finishSubscription = (hasPlan, wantsPlan) ->
|
|
return callback(null, req, user) if hasPlan is wantsPlan
|
|
if wantsPlan and not hasPlan
|
|
SubscriptionHandler.subscribeUser(req, user, (err) ->
|
|
return callback(err) if err
|
|
return callback(null, req, user)
|
|
)
|
|
else if hasPlan and not wantsPlan
|
|
SubscriptionHandler.unsubscribeUser(req, user, (err) ->
|
|
return callback(err) if err
|
|
return callback(null, req, user)
|
|
)
|
|
if req.body.stripe.subscribeEmails?
|
|
SubscriptionHandler.subscribeUser(req, user, (err) ->
|
|
return callback(err) if err
|
|
return callback(null, req, user)
|
|
)
|
|
else if req.body.stripe.unsubscribeEmail?
|
|
SubscriptionHandler.unsubscribeUser(req, user, (err) ->
|
|
return callback(err) if err
|
|
return callback(null, req, user)
|
|
)
|
|
else
|
|
wantsPlan = req.body.stripe.planID?
|
|
hasPlan = user.get('stripe')?.planID? and not req.body.stripe.prepaidCode?
|
|
finishSubscription hasPlan, wantsPlan
|
|
|
|
# Discount setting
|
|
(req, user, callback) ->
|
|
return callback(null, req, user) unless req.body.stripe
|
|
return callback(null, req, user) unless req.user?.isAdmin()
|
|
hasCoupon = user.get('stripe')?.couponID
|
|
wantsCoupon = req.body.stripe.couponID
|
|
|
|
return callback(null, req, user) if hasCoupon is wantsCoupon
|
|
if wantsCoupon and (hasCoupon isnt wantsCoupon)
|
|
DiscountHandler.discountUser(req, user, (err) ->
|
|
return callback(err) if err
|
|
return callback(null, req, user)
|
|
)
|
|
else if hasCoupon and not wantsCoupon
|
|
DiscountHandler.removeDiscountFromCustomer(req, user, (err) ->
|
|
return callback(err) if err
|
|
return callback(null, req, user)
|
|
)
|
|
|
|
]
|
|
|
|
getById: (req, res, id) ->
|
|
if Handler.isID(id) and req.user?._id.equals(id)
|
|
return @sendSuccess(res, @formatEntity(req, req.user))
|
|
super(req, res, id)
|
|
|
|
getByIDs: (req, res) ->
|
|
return @sendForbiddenError(res) unless req.user?.isAdmin()
|
|
User.find {_id: {$in: req.body.ids}}, (err, users) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
cleandocs = (@formatEntity(req, doc) for doc in users)
|
|
@sendSuccess(res, cleandocs)
|
|
|
|
getNamesByIDs: (req, res) ->
|
|
ids = req.query.ids or req.body.ids
|
|
returnWizard = req.query.wizard or req.body.wizard
|
|
properties = if returnWizard then 'name wizard firstName lastName' else 'name firstName lastName'
|
|
@getPropertiesFromMultipleDocuments res, User, properties, ids
|
|
|
|
nameToID: (req, res, name) ->
|
|
User.findOne({nameLower: unescape(name).toLowerCase(), anonymous: false}).exec (err, otherUser) ->
|
|
res.send(if otherUser then otherUser._id else JSON.stringify(''))
|
|
res.end()
|
|
|
|
getSimulatorLeaderboard: (req, res) ->
|
|
queryParameters = @getSimulatorLeaderboardQueryParameters(req)
|
|
leaderboardQuery = User.find(queryParameters.query).select('name simulatedBy simulatedFor').sort({'simulatedBy': queryParameters.sortOrder}).limit(queryParameters.limit)
|
|
leaderboardQuery.cache(10 * 60 * 1000) if req.query.scoreOffset is -1
|
|
leaderboardQuery.exec (err, otherUsers) ->
|
|
otherUsers = _.reject otherUsers, _id: req.user._id if req.query.scoreOffset isnt -1 and req.user
|
|
otherUsers ?= []
|
|
res.send(otherUsers)
|
|
res.end()
|
|
|
|
getMySimulatorLeaderboardRank: (req, res) ->
|
|
req.query.order = 1
|
|
queryParameters = @getSimulatorLeaderboardQueryParameters(req)
|
|
User.count queryParameters.query, (err, count) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
res.send JSON.stringify(count + 1)
|
|
|
|
getSimulatorLeaderboardQueryParameters: (req) ->
|
|
@validateSimulateLeaderboardRequestParameters(req)
|
|
|
|
query = {}
|
|
sortOrder = -1
|
|
limit = if req.query.limit > 30 then 30 else req.query.limit
|
|
if req.query.scoreOffset isnt -1
|
|
simulatedByQuery = {}
|
|
simulatedByQuery[if req.query.order is 1 then '$gt' else '$lte'] = req.query.scoreOffset
|
|
query.simulatedBy = simulatedByQuery
|
|
sortOrder = 1 if req.query.order is 1
|
|
else
|
|
query.simulatedBy = {'$exists': true}
|
|
{query: query, sortOrder: sortOrder, limit: limit}
|
|
|
|
validateSimulateLeaderboardRequestParameters: (req) ->
|
|
req.query.order = parseInt(req.query.order) ? -1
|
|
req.query.scoreOffset = parseFloat(req.query.scoreOffset) ? 100000
|
|
req.query.limit = parseInt(req.query.limit) ? 20
|
|
|
|
post: (req, res) ->
|
|
return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body)
|
|
return @sendBadInputError(res, 'Must have an anonymous user to post with.') unless req.user
|
|
return @sendBadInputError(res, 'Existing users cannot create new ones.') if req.user.get('anonymous') is false
|
|
req.body._id = req.user._id if req.user.get('anonymous')
|
|
@put(req, res)
|
|
|
|
hasAccessToDocument: (req, document) ->
|
|
if req.route.method in ['put', 'post', 'patch', 'delete']
|
|
return true if req.user?.isAdmin()
|
|
return req.user?._id.equals(document._id)
|
|
return true
|
|
|
|
delete: (req, res, userID) ->
|
|
# Instead of just deleting the User object, we should remove all the properties except for _id
|
|
# And add a `deleted: true` property
|
|
@getDocumentForIdOrSlug userID, (err, user) => # Check first
|
|
return @sendDatabaseError res, err if err
|
|
return @sendNotFoundError res unless user
|
|
return @sendForbiddenError res unless @hasAccessToDocument(req, user)
|
|
|
|
# Delete subscriptions attached to this user first
|
|
# TODO: check sponsored subscriptions (stripe.recipients)
|
|
checkPersonalSubscription = (user, done) =>
|
|
try
|
|
return done() unless user.get('stripe')?.subscriptionID?
|
|
SubscriptionHandler.unsubscribeUser {body: user.toObject()}, user, (err) =>
|
|
log.error("User delete check personal sub " + err) if err
|
|
done()
|
|
catch error
|
|
log.error("User delete check personal sub " + error)
|
|
done()
|
|
checkRecipientSubscription = (user, done) =>
|
|
try
|
|
return done() unless sponsorID = user.get('stripe')?.sponsorID
|
|
User.findById sponsorID, (err, sponsor) =>
|
|
if err
|
|
log.error("User delete check recipient sub " + err)
|
|
return done()
|
|
unless sponsor
|
|
log.error("User delete check recipient sub no sponsor #{user.get('stripe').sponsorID}")
|
|
return done()
|
|
sponsorObject = sponsor.toObject()
|
|
sponsorObject.stripe.unsubscribeEmail = user.get('email')
|
|
SubscriptionHandler.unsubscribeRecipient {body: sponsorObject}, sponsor, (err) =>
|
|
log.error("User delete check recipient sub " + err) if err
|
|
done()
|
|
catch error
|
|
log.error("User delete check recipient sub " + error)
|
|
done()
|
|
deleteSubscriptions = (user, done) =>
|
|
checkPersonalSubscription user, (err) =>
|
|
checkRecipientSubscription user, done
|
|
|
|
deleteSubscriptions user, =>
|
|
obj = user.toObject()
|
|
for prop, val of obj
|
|
user.set(prop, undefined) unless prop is '_id'
|
|
user.set('dateDeleted', new Date())
|
|
user.set('deleted', true)
|
|
|
|
# Hack to get saving of Users to work. Probably should replace these props with strings
|
|
# so that validation doesn't get hung up on Date objects in the documents.
|
|
delete obj.dateCreated
|
|
|
|
user.save (err) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
@sendNoContent res
|
|
|
|
getByRelationship: (req, res, args...) ->
|
|
return @agreeToCLA(req, res) if args[1] is 'agreeToCLA'
|
|
return @agreeToEmployerAgreement(req, res) if args[1] is 'agreeToEmployerAgreement'
|
|
return @avatar(req, res, args[0]) if args[1] is 'avatar'
|
|
return @getByIDs(req, res) if args[1] is 'users'
|
|
return @getNamesByIDs(req, res) if args[1] is 'names'
|
|
return @getPrepaidCodes(req, res) if args[1] is 'prepaid_codes'
|
|
return @getSchoolCounts(req, res) if args[1] is 'school_counts'
|
|
return @nameToID(req, res, args[0]) if args[1] is 'nameToID'
|
|
return @getLevelSessionsForEmployer(req, res, args[0]) if args[1] is 'level.sessions' and args[2] is 'employer'
|
|
return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions'
|
|
return @getCandidates(req, res) if args[1] is 'candidates'
|
|
return @getClans(req, res, args[0]) if args[1] is 'clans'
|
|
return @getCourseInstances(req, res, args[0]) if args[1] is 'course_instances'
|
|
return @getEmployers(req, res) if args[1] is 'employers'
|
|
return @getSimulatorLeaderboard(req, res, args[0]) if args[1] is 'simulatorLeaderboard'
|
|
return @getMySimulatorLeaderboardRank(req, res, args[0]) if args[1] is 'simulator_leaderboard_rank'
|
|
return @getEarnedAchievements(req, res, args[0]) if args[1] is 'achievements'
|
|
return @getRecentlyPlayed(req, res, args[0]) if args[1] is 'recently_played'
|
|
return @trackActivity(req, res, args[0], args[2], args[3]) if args[1] is 'track' and args[2]
|
|
return @getRemark(req, res, args[0]) if args[1] is 'remark'
|
|
return @searchForUser(req, res) if args[1] is 'admin_search'
|
|
return @getStripeInfo(req, res, args[0]) if args[1] is 'stripe'
|
|
return @getSubRecipients(req, res) if args[1] is 'sub_recipients'
|
|
return @getSubSponsor(req, res) if args[1] is 'sub_sponsor'
|
|
return @getSubSponsors(req, res) if args[1] is 'sub_sponsors'
|
|
return @sendOneTimeEmail(req, res, args[0]) if args[1] is 'send_one_time_email'
|
|
return @resetProgress(req, res, args[0]) if args[1] is 'reset_progress'
|
|
return @sendNotFoundError(res)
|
|
super(arguments...)
|
|
|
|
getStripeInfo: (req, res, handle) ->
|
|
@getDocumentForIdOrSlug handle, (err, user) =>
|
|
return @sendNotFoundError(res) if not user
|
|
return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or req.user.get('_id').equals(user.get('_id')))
|
|
return @sendNotFoundError(res) if not customerID = user.get('stripe')?.customerID
|
|
stripe.customers.retrieve customerID, (err, customer) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
info = card: customer.sources?.data?[0]
|
|
findStripeSubscription customerID, subscriptionID: user.get('stripe').subscriptionID, (subscription) =>
|
|
info.subscription = subscription
|
|
findStripeSubscription customerID, subscriptionID: user.get('stripe').sponsorSubscriptionID, (subscription) =>
|
|
info.sponsorSubscription = subscription
|
|
@sendSuccess(res, JSON.stringify(info, null, '\t'))
|
|
|
|
getSubRecipients: (req, res) ->
|
|
# Return map of userIDs to name/email/cancel date
|
|
# TODO: Add test for this API
|
|
|
|
return @sendSuccess(res, {}) if _.isEmpty(req.user?.get('stripe')?.recipients ? [])
|
|
return @sendSuccess(res, {}) unless req.user.get('stripe')?.customerID?
|
|
|
|
# Get recipients User info
|
|
ids = (recipient.userID for recipient in req.user.get('stripe').recipients)
|
|
User.find({'_id': { $in: ids} }, 'name emailLower').exec (err, users) =>
|
|
info = {}
|
|
_.each users, (user) -> info[user.id] = user.toObject()
|
|
customerID = req.user.get('stripe').customerID
|
|
|
|
nextBatch = (starting_after, done) ->
|
|
options = limit: 100
|
|
options.starting_after = starting_after if starting_after
|
|
stripe.customers.listSubscriptions customerID, options, (err, subscriptions) ->
|
|
return done(err) if err
|
|
return done() unless subscriptions?.data?.length > 0
|
|
for sub in subscriptions.data
|
|
userID = sub.metadata?.id
|
|
continue unless userID of info
|
|
if sub.cancel_at_period_end and info[userID]['cancel_at_period_end'] isnt false
|
|
info[userID]['cancel_at_period_end'] = new Date(sub.current_period_end * 1000)
|
|
else
|
|
info[userID]['cancel_at_period_end'] = false
|
|
|
|
if subscriptions.has_more
|
|
return nextBatch(subscriptions.data[subscriptions.data.length - 1].id, done)
|
|
else
|
|
return done()
|
|
nextBatch null, (err) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
@sendSuccess(res, info)
|
|
|
|
getSubSponsor: (req, res) ->
|
|
# TODO: Add test for this API
|
|
|
|
return @sendSuccess(res, {}) unless req.user?.get('stripe')?.sponsorID?
|
|
|
|
# Get sponsor User info
|
|
User.findById req.user.get('stripe').sponsorID, (err, sponsor) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
return @sendDatabaseError(res, 'No sponsor customerID') unless sponsor?.get('stripe')?.customerID?
|
|
info =
|
|
email: sponsor.get('emailLower')
|
|
name: sponsor.get('name')
|
|
|
|
# Get recipient subscription info
|
|
findStripeSubscription sponsor.get('stripe')?.customerID, userID: req.user.id, (subscription) =>
|
|
info.subscription = subscription
|
|
@sendDatabaseError(res, 'No sponsored subscription found') unless info.subscription?
|
|
@sendSuccess(res, info)
|
|
|
|
getSubSponsors: (req, res) ->
|
|
return @sendForbiddenError(res) unless req.user?.isAdmin()
|
|
Payment.find {$where: 'this.purchaser.valueOf() != this.recipient.valueOf()'}, (err, payments) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
sponsorIDs = (payment.get('purchaser') for payment in payments)
|
|
User.find {$and: [{_id: {$in: sponsorIDs}}, {"stripe.sponsorSubscriptionID": {$exists: true}}]}, (err, users) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
sponsors = (@formatEntity(req, doc) for doc in users when doc.get('stripe').recipients?.length > 0)
|
|
@sendSuccess(res, sponsors)
|
|
|
|
sendOneTimeEmail: (req, res) ->
|
|
# TODO: Should this API be somewhere else?
|
|
# TODO: Where should email types be stored?
|
|
# TODO: How do we schema validate an update db call?
|
|
|
|
return @sendForbiddenError(res) unless req.user
|
|
email = req.query.email or req.body.email
|
|
type = req.query.type or req.body.type
|
|
return @sendBadInputError res, 'No email given.' unless email?
|
|
return @sendBadInputError res, 'No type given.' unless type?
|
|
|
|
# log.warn "sendOneTimeEmail #{type} #{email}"
|
|
|
|
unless type in ['subscribe modal parent', 'share progress modal parent']
|
|
return @sendBadInputError res, "Unknown one-time email type #{type}"
|
|
|
|
sendMail = (emailParams) =>
|
|
sendwithus.api.send emailParams, (err, result) =>
|
|
if err
|
|
log.error "sendwithus one-time email error: #{err}, result: #{result}"
|
|
return @sendError res, 500, 'send mail failed.'
|
|
req.user.update {$push: {"emails.oneTimes": {type: type, email: email, sent: new Date()}}}, (err) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
@sendSuccess(res, {result: 'success'})
|
|
AnalyticsLogEvent.logEvent req.user, 'Sent one time email', email: email, type: type
|
|
|
|
# Generic email data
|
|
emailParams =
|
|
recipient:
|
|
address: email
|
|
email_data:
|
|
name: req.user.get('name') or ''
|
|
if codeLanguage = req.user.get('aceConfig.language')
|
|
codeLanguage = codeLanguage[0].toUpperCase() + codeLanguage.slice(1)
|
|
codeLanguage = codeLanguage.replace 'script', 'Script'
|
|
emailParams['email_data']['codeLanguage'] = codeLanguage
|
|
if senderEmail = req.user.get('email')
|
|
emailParams['email_data']['senderEmail'] = senderEmail
|
|
|
|
# Type-specific email data
|
|
if type is 'subscribe modal parent'
|
|
emailParams['email_id'] = sendwithus.templates.parent_subscribe_email
|
|
else if type is 'share progress modal parent'
|
|
emailParams['email_id'] = sendwithus.templates.share_progress_email
|
|
|
|
sendMail emailParams
|
|
|
|
getPrepaidCodes: (req, res) ->
|
|
return @sendSuccess(res, []) unless req.user?
|
|
orQuery = [{ creator: req.user._id }, { 'redeemers.userID' : req.user._id }]
|
|
Prepaid.find({}).or(orQuery).exec (err, documents) =>
|
|
@sendSuccess(res, documents)
|
|
|
|
getSchoolCounts: (req, res) ->
|
|
return @sendSuccess(res, []) unless req.user?.isAdmin()
|
|
minCount = req.body.minCount ? 20
|
|
query = {$and: [
|
|
{anonymous: false},
|
|
{schoolName: {$exists: true}},
|
|
{schoolName: {$ne: ''}}
|
|
]}
|
|
User.find(query, {schoolName: 1}).exec (err, documents) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
schoolCountMap = {}
|
|
for doc in documents
|
|
schoolName = doc.get('schoolName')
|
|
schoolCountMap[schoolName] ?= 0;
|
|
schoolCountMap[schoolName]++;
|
|
schoolCounts = []
|
|
for schoolName, count of schoolCountMap
|
|
continue unless count >= minCount
|
|
schoolCounts.push schoolName: schoolName, count: count
|
|
@sendSuccess(res, schoolCounts)
|
|
|
|
agreeToCLA: (req, res) ->
|
|
return @sendForbiddenError(res) unless req.user
|
|
doc =
|
|
user: req.user._id+''
|
|
email: req.user.get 'email'
|
|
name: req.user.get 'name'
|
|
githubUsername: req.body.githubUsername
|
|
created: new Date()+''
|
|
collection = mongoose.connection.db.collection 'cla.submissions', (err, collection) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
collection.insert doc, (err) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
req.user.set('signedCLA', doc.created)
|
|
req.user.save (err) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
@sendSuccess(res, {result: 'success'})
|
|
slack.sendSlackMessage "#{req.body.githubUsername or req.user.get('name')} just signed the CLA.", ['dev-feed']
|
|
|
|
avatar: (req, res, id) ->
|
|
if not isID(id)
|
|
return @sendBadInputError(res, 'Invalid avatar id')
|
|
@modelClass.findById(id).exec (err, document) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
return @sendNotFoundError(res) unless document
|
|
photoURL = document?.get('photoURL')
|
|
if photoURL
|
|
photoURL = "/file/#{photoURL}"
|
|
else if req.query.employerPageAvatar is "true"
|
|
photoURL = @buildGravatarURL document, req.query.s, "/images/pages/employer/anon_user.png"
|
|
else
|
|
photoURL = @buildGravatarURL document, req.query.s, req.query.fallback
|
|
res.redirect photoURL
|
|
res.end()
|
|
|
|
getLevelSessionsForEmployer: (req, res, userID) ->
|
|
return @sendForbiddenError(res) unless req.user
|
|
return @sendForbiddenError(res) unless req.user._id+'' is userID or req.user.isAdmin() or ('employer' in (req.user.get('permissions') ? []))
|
|
query = creator: userID, levelID: {$in: ['criss-cross', 'gridmancer', 'greed', 'dungeon-arena', 'brawlwood', 'gold-rush']}
|
|
projection = 'levelName levelID team playtime codeLanguage submitted code totalScore teamSpells level'
|
|
LevelSession.find(query).select(projection).exec (err, documents) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
|
|
@sendSuccess(res, documents)
|
|
|
|
IDify: (idOrSlug, done) ->
|
|
return done null, idOrSlug if Handler.isID idOrSlug
|
|
User.findBySlug idOrSlug, (err, user) -> done err, user?.get '_id'
|
|
|
|
getLevelSessions: (req, res, userIDOrSlug) ->
|
|
@IDify userIDOrSlug, (err, userID) =>
|
|
return @sendDatabaseError res, err if err
|
|
return @sendNotFoundError res unless userID?
|
|
query = creator: userID + ''
|
|
isAuthorized = req.user?._id+'' is userID or req.user?.isAdmin()
|
|
projection = {}
|
|
if req.query.project
|
|
projection[field] = 1 for field in req.query.project.split(',') when isAuthorized or not (field in LevelSessionHandler.privateProperties)
|
|
else unless isAuthorized
|
|
projection[field] = 0 for field in LevelSessionHandler.privateProperties
|
|
|
|
LevelSession.find(query).select(projection).exec (err, documents) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
if req.query.order
|
|
documents = _.sortBy documents, 'changed'
|
|
if req.query.order + '' is '-1'
|
|
documents.reverse()
|
|
documents = (LevelSessionHandler.formatEntity(req, doc) for doc in documents)
|
|
@sendSuccess(res, documents)
|
|
|
|
getEarnedAchievements: (req, res, userIDOrSlug) ->
|
|
@IDify userIDOrSlug, (err, userID) =>
|
|
return @sendDatabaseError res, err if err
|
|
return @sendNotFoundError res unless userID?
|
|
query = user: userID + ''
|
|
query.notified = false if req.query.notified is 'false'
|
|
EarnedAchievement.find(query).sort(changed: -1).exec (err, documents) =>
|
|
return @sendDatabaseError(res, err) if err?
|
|
cleandocs = (@formatEntity(req, doc) for doc in documents)
|
|
@sendSuccess(res, cleandocs)
|
|
|
|
getRecentlyPlayed: (req, res, userID) ->
|
|
twoWeeksAgo = moment().subtract('days', 14).toDate()
|
|
LevelSession.find(creator: userID, changed: $gt: twoWeeksAgo).sort(changed: -1).exec (err, docs) =>
|
|
return @sendDatabaseError res, err if err?
|
|
cleandocs = (@formatEntity(req, doc) for doc in docs)
|
|
@sendSuccess res, cleandocs
|
|
|
|
trackActivity: (req, res, userID, activityName, increment=1) ->
|
|
return @sendMethodNotAllowed res unless req.method is 'POST'
|
|
isMe = userID is req.user?._id + ''
|
|
isAuthorized = isMe or req.user?.isAdmin()
|
|
isAuthorized ||= ('employer' in (req.user?.get('permissions') ? [])) and (activityName in ['viewed_by_employer', 'contacted_by_employer'])
|
|
return @sendForbiddenError res unless isAuthorized
|
|
updateUser = (user) =>
|
|
activity = user.trackActivity activityName, increment
|
|
user.update {activity: activity}, (err) =>
|
|
return @sendDatabaseError res, err if err
|
|
@sendSuccess res, result: 'success'
|
|
if isMe
|
|
updateUser(req.user)
|
|
else
|
|
@getDocumentForIdOrSlug userID, (err, user) =>
|
|
return @sendDatabaseError res, err if err
|
|
return @sendNotFoundError res unless user
|
|
updateUser user
|
|
|
|
agreeToEmployerAgreement: (req, res) ->
|
|
userIsAnonymous = req.user?.get('anonymous')
|
|
if userIsAnonymous then return errors.unauthorized(res, 'You need to be logged in to agree to the employer agreeement.')
|
|
profileData = req.body
|
|
#TODO: refactor this bit to make it more elegant
|
|
if not profileData.id or not profileData.positions or not profileData.emailAddress or not profileData.firstName or not profileData.lastName
|
|
return errors.badInput(res, 'You need to have a more complete profile to sign up for this service.')
|
|
@modelClass.findById(req.user.id).exec (err, user) =>
|
|
if user.get('employerAt') or user.get('signedEmployerAgreement') or 'employer' in (user.get('permissions') ? [])
|
|
return errors.conflict(res, 'You already have signed the agreement!')
|
|
#TODO: Search for the current position
|
|
employerAt = _.filter(profileData.positions.values, 'isCurrent')[0]?.company.name ? 'Not available'
|
|
signedEmployerAgreement =
|
|
linkedinID: profileData.id
|
|
date: new Date()
|
|
data: profileData
|
|
updateObject =
|
|
'employerAt': employerAt
|
|
'signedEmployerAgreement': signedEmployerAgreement
|
|
$push: 'permissions': 'employer'
|
|
|
|
User.update {'_id': req.user.id}, updateObject, (err, result) =>
|
|
if err? then return errors.serverError(res, "There was an issue updating the user object to reflect employer status: #{err}")
|
|
res.send({'message': 'The agreement was successful.'})
|
|
res.end()
|
|
|
|
getCandidates: (req, res) ->
|
|
return @sendForbiddenError(res) unless req.user
|
|
return @sendForbiddenError(res) # No one can view the candidates, since in a rush, we deleted their index!
|
|
authorized = req.user.isAdmin() or ('employer' in (req.user.get('permissions') ? []))
|
|
months = if req.user.isAdmin() then 12 else 2
|
|
since = (new Date((new Date()) - months * 30.4 * 86400 * 1000)).toISOString()
|
|
query = {'jobProfile.updated': {$gt: since}}
|
|
query['jobProfile.active'] = true unless req.user.isAdmin()
|
|
selection = 'jobProfile jobProfileApproved photoURL'
|
|
selection += ' email name' if authorized
|
|
User.find(query).select(selection).exec (err, documents) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
candidates = (candidate for candidate in documents when @employerCanViewCandidate req.user, candidate.toObject())
|
|
candidates = (@formatCandidate(authorized, candidate) for candidate in candidates)
|
|
@sendSuccess(res, candidates)
|
|
|
|
getClans: (req, res, userIDOrSlug) ->
|
|
@getDocumentForIdOrSlug userIDOrSlug, (err, user) =>
|
|
return @sendNotFoundError(res) unless user
|
|
clanIDs = user.get('clans') ? []
|
|
query = {$and: [{_id: {$in: clanIDs}}]}
|
|
query['$and'].push {type: 'public'} unless req.user?.id is user.id
|
|
Clan.find query, (err, documents) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
@sendSuccess(res, documents)
|
|
|
|
getCourseInstances: (req, res, userIDOrSlug) ->
|
|
@getDocumentForIdOrSlug userIDOrSlug, (err, user) =>
|
|
return @sendNotFoundError(res) unless user
|
|
CourseInstance.find {members: {$in: [user.get('_id')]}}, (err, documents) =>
|
|
return @sendDatabaseError(res, err) if err
|
|
@sendSuccess(res, documents)
|
|
|
|
formatCandidate: (authorized, document) ->
|
|
fields = if authorized then ['name', 'jobProfile', 'jobProfileApproved', 'photoURL', '_id'] else ['_id','jobProfile', 'jobProfileApproved']
|
|
obj = _.pick document.toObject(), fields
|
|
obj.photoURL ||= obj.jobProfile.photoURL #if authorized
|
|
subfields = ['country', 'city', 'lookingFor', 'jobTitle', 'skills', 'experience', 'updated', 'active', 'shortDescription', 'curated', 'visa']
|
|
if authorized
|
|
subfields = subfields.concat ['name']
|
|
obj.jobProfile = _.pick obj.jobProfile, subfields
|
|
obj
|
|
|
|
employerCanViewCandidate: (employer, candidate) ->
|
|
return true if employer.isAdmin()
|
|
for job in candidate.jobProfile?.work ? []
|
|
# TODO: be smarter about different ways to write same company names to ensure privacy.
|
|
# We'll have to manually pay attention to how we set employer names for now.
|
|
if job.employer?.toLowerCase() is employer.get('employerAt')?.toLowerCase()
|
|
log.info "#{employer.get('name')} at #{employer.get('employerAt')} can't see #{candidate.jobProfile.name} because s/he worked there."
|
|
return false if job.employer?.toLowerCase() is employer.get('employerAt')?.toLowerCase()
|
|
true
|
|
|
|
getEmployers: (req, res) ->
|
|
return @sendForbiddenError(res) unless req.user?.isAdmin()
|
|
return @sendForbiddenError(res) # No one can view the employers, since in a rush, we deleted their index!
|
|
query = {employerAt: {$exists: true, $ne: ''}}
|
|
selection = 'name firstName lastName email activity signedEmployerAgreement photoURL employerAt'
|
|
User.find(query).select(selection).lean().exec (err, documents) =>
|
|
return @sendDatabaseError res, err if err
|
|
@sendSuccess res, documents
|
|
|
|
buildGravatarURL: (user, size, fallback) ->
|
|
emailHash = @buildEmailHash user
|
|
fallback ?= 'http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png'
|
|
fallback = "http://codecombat.com#{fallback}" unless /^http/.test fallback
|
|
"https://www.gravatar.com/avatar/#{emailHash}?s=#{size}&default=#{fallback}"
|
|
|
|
buildEmailHash: (user) ->
|
|
# emailHash is used by gravatar
|
|
hash = crypto.createHash('md5')
|
|
if user.get('email')
|
|
hash.update(_.trim(user.get('email')).toLowerCase())
|
|
else
|
|
hash.update(user.get('_id') + '')
|
|
hash.digest('hex')
|
|
|
|
getRemark: (req, res, userID) ->
|
|
return @sendForbiddenError(res) unless req.user?.isAdmin()
|
|
query = user: userID
|
|
projection = null
|
|
if req.query.project
|
|
projection = {}
|
|
projection[field] = 1 for field in req.query.project.split(',')
|
|
UserRemark.findOne(query).select(projection).exec (err, remark) =>
|
|
return @sendDatabaseError res, err if err
|
|
return @sendNotFoundError res unless remark?
|
|
@sendSuccess res, remark
|
|
|
|
searchForUser: (req, res) ->
|
|
# TODO: also somehow search the CLAs to find a match amongst those fields and to find GitHub ids
|
|
return @sendForbiddenError(res) unless req.user?.isAdmin()
|
|
search = req.body.search
|
|
query = email: {$exists: true}, $or: [
|
|
{emailLower: search}
|
|
{nameLower: search}
|
|
]
|
|
query.$or.push {_id: mongoose.Types.ObjectId(search) if isID search}
|
|
if search.length > 5
|
|
searchParts = search.split(/[.+@]/)
|
|
if searchParts.length > 1
|
|
query.$or.push {emailLower: {$regex: '^' + searchParts[0]}}
|
|
projection = name: 1, email: 1, dateCreated: 1
|
|
User.find(query).select(projection).lean().exec (err, users) =>
|
|
return @sendDatabaseError res, err if err
|
|
@sendSuccess res, users
|
|
|
|
resetProgress: (req, res, userID) ->
|
|
return @sendMethodNotAllowed res unless req.method is 'POST'
|
|
return @sendForbiddenError res unless userID and userID is req.user?._id + '' # Only you can reset your own progress
|
|
return @sendForbiddenError res if req.user?.isAdmin() # Protect admins from resetting their progress
|
|
@constructor.resetProgressForUser req.user, (err, results) =>
|
|
return @sendDatabaseError res, err if err
|
|
@sendSuccess res, result: 'success'
|
|
|
|
@resetProgressForUser: (user, cb) ->
|
|
async.parallel [
|
|
(cb) -> LevelSession.remove {creator: user._id + ''}, cb
|
|
(cb) -> EarnedAchievement.remove {user: user._id + ''}, cb
|
|
(cb) -> UserPollsRecord.remove {user: user._id + ''}, cb
|
|
(cb) -> user.update {points: 0, 'stats.gamesCompleted': 0, 'stats.concepts': {}, 'earned.gems': 0, 'earned.levels': [], 'earned.items': [], 'earned.heroes': [], 'purchased.items': [], 'purchased.heroes': [], spent: 0}, cb
|
|
], cb
|
|
|
|
countEdits = (model, done) ->
|
|
statKey = User.statsMapping.edits[model.modelName]
|
|
return done(new Error 'Could not resolve statKey for model') unless statKey?
|
|
userStream = User.find({anonymous: false}).sort('_id').stream()
|
|
streamFinished = false
|
|
usersTotal = 0
|
|
usersFinished = 0
|
|
numberRunning = 0
|
|
doneWithUser = (err) ->
|
|
log.error err if err?
|
|
++usersFinished
|
|
--numberRunning
|
|
userStream.resume()
|
|
done?() if streamFinished and usersFinished is usersTotal
|
|
userStream.on 'error', (err) -> log.error err
|
|
userStream.on 'close', -> streamFinished = true
|
|
userStream.on 'data', (user) ->
|
|
++usersTotal
|
|
++numberRunning
|
|
userStream.pause() if numberRunning > 20
|
|
userObjectID = user.get('_id')
|
|
userStringID = userObjectID.toHexString()
|
|
|
|
model.count {$or: [creator: userObjectID, creator: userStringID]}, (err, count) ->
|
|
if count
|
|
update = $set: {}
|
|
update.$set[statKey] = count
|
|
else
|
|
update = $unset: {}
|
|
update.$unset[statKey] = ''
|
|
console.log "... updating #{userStringID} patches #{statKey} to #{count}, #{usersTotal} players found so far." if count
|
|
User.findByIdAndUpdate user.get('_id'), update, (err) ->
|
|
log.error err if err?
|
|
doneWithUser()
|
|
|
|
# I don't like leaking big variables, could remove this for readability
|
|
# Meant for passing into MongoDB
|
|
{isMiscPatch, isTranslationPatch} = do ->
|
|
deltas = require '../../app/core/deltas'
|
|
|
|
isMiscPatch: (obj) ->
|
|
expanded = deltas.flattenDelta obj.get 'delta'
|
|
_.some expanded, (delta) -> 'i18n' not in delta.dataPath
|
|
isTranslationPatch: (obj) ->
|
|
expanded = deltas.flattenDelta obj.get 'delta'
|
|
_.some expanded, (delta) -> 'i18n' in delta.dataPath
|
|
|
|
Patch = require '../models/Patch'
|
|
# filter is passed a mongoose document and should return a boolean,
|
|
# determining whether the patch should be counted
|
|
countPatchesByUsersInMemory = (query, filter, statName, done) ->
|
|
updateUser = (user, count, doneUpdatingUser) ->
|
|
method = if count then '$set' else '$unset'
|
|
update = {}
|
|
update[method] = {}
|
|
update[method][statName] = count or ''
|
|
console.log "... updating #{user.get('_id')} patches #{JSON.stringify(query)} #{statName} to #{count}, #{usersTotal} players found so far." if count
|
|
User.findByIdAndUpdate user.get('_id'), update, doneUpdatingUser
|
|
|
|
userStream = User.find({anonymous: false}).sort('_id').stream()
|
|
streamFinished = false
|
|
usersTotal = 0
|
|
usersFinished = 0
|
|
numberRunning = 0
|
|
doneWithUser = (err) ->
|
|
log.error err if err?
|
|
++usersFinished
|
|
--numberRunning
|
|
userStream.resume()
|
|
done?() if streamFinished and usersFinished is usersTotal
|
|
userStream.on 'error', (err) -> log.error err
|
|
userStream.on 'close', -> streamFinished = true
|
|
userStream.on 'data', (user) ->
|
|
++usersTotal
|
|
++numberRunning
|
|
userStream.pause() if numberRunning > 20
|
|
userObjectID = user.get '_id'
|
|
userStringID = userObjectID.toHexString()
|
|
# Extend query with a patch ownership test
|
|
_.extend query, {$or: [{creator: userObjectID}, {creator: userStringID}]}
|
|
|
|
count = 0
|
|
stream = Patch.where(query).stream()
|
|
stream.on 'data', (doc) -> ++count if filter doc
|
|
stream.on 'error', (err) ->
|
|
updateUser user, count, doneWithUser
|
|
log.error "Recalculating #{statName} for user #{user} stopped prematurely because of error"
|
|
stream.on 'close', ->
|
|
updateUser user, count, doneWithUser
|
|
|
|
countPatchesByUsers = (query, statName, done) ->
|
|
Patch = require '../models/Patch'
|
|
|
|
userStream = User.find({anonymous: false}).sort('_id').stream()
|
|
streamFinished = false
|
|
usersTotal = 0
|
|
usersFinished = 0
|
|
numberRunning = 0
|
|
doneWithUser = (err) ->
|
|
log.error err if err?
|
|
++usersFinished
|
|
--numberRunning
|
|
userStream.resume()
|
|
done?() if streamFinished and usersFinished is usersTotal
|
|
userStream.on 'error', (err) -> log.error err
|
|
userStream.on 'close', -> streamFinished = true
|
|
userStream.on 'data', (user) ->
|
|
++usersTotal
|
|
++numberRunning
|
|
userStream.pause() if numberRunning > 20
|
|
userObjectID = user.get '_id'
|
|
userStringID = userObjectID.toHexString()
|
|
# Extend query with a patch ownership test
|
|
_.extend query, {$or: [{creator: userObjectID}, {creator: userStringID}]}
|
|
|
|
Patch.count query, (err, count) ->
|
|
method = if count then '$set' else '$unset'
|
|
update = {}
|
|
update[method] = {}
|
|
update[method][statName] = count or ''
|
|
console.log "... updating #{userStringID} patches #{query} to #{count}, #{usersTotal} players found so far." if count
|
|
User.findByIdAndUpdate user.get('_id'), update, doneWithUser
|
|
|
|
statRecalculators:
|
|
gamesCompleted: (done) ->
|
|
LevelSession = require '../models/LevelSession'
|
|
|
|
userStream = User.find({anonymous: false}).sort('_id').stream()
|
|
streamFinished = false
|
|
usersTotal = 0
|
|
usersFinished = 0
|
|
numberRunning = 0
|
|
doneWithUser = (err) ->
|
|
log.error err if err?
|
|
++usersFinished
|
|
--numberRunning
|
|
userStream.resume()
|
|
if streamFinished and usersFinished is usersTotal
|
|
console.log "----------- Finished recalculating statistics for gamesCompleted for #{usersFinished} players. -----------"
|
|
done?()
|
|
userStream.on 'error', (err) -> log.error err
|
|
userStream.on 'close', -> streamFinished = true
|
|
userStream.on 'data', (user) ->
|
|
++usersTotal
|
|
++numberRunning
|
|
userStream.pause() if numberRunning > 20
|
|
userID = user.get('_id').toHexString()
|
|
|
|
LevelSession.count {creator: userID, 'state.complete': true}, (err, count) ->
|
|
update = if count then {$set: 'stats.gamesCompleted': count} else {$unset: 'stats.gamesCompleted': ''}
|
|
console.log "... updating #{userID} gamesCompleted to #{count}, #{usersTotal} players found so far." if Math.random() < 0.001
|
|
User.findByIdAndUpdate user.get('_id'), update, doneWithUser
|
|
|
|
articleEdits: (done) ->
|
|
Article = require '../models/Article'
|
|
countEdits Article, done
|
|
|
|
levelEdits: (done) ->
|
|
Level = require '../models/Level'
|
|
countEdits Level, done
|
|
|
|
levelComponentEdits: (done) ->
|
|
LevelComponent = require '../models/LevelComponent'
|
|
countEdits LevelComponent, done
|
|
|
|
levelSystemEdits: (done) ->
|
|
LevelSystem = require '../models/LevelSystem'
|
|
countEdits LevelSystem, done
|
|
|
|
thangTypeEdits: (done) ->
|
|
ThangType = require '../models/ThangType'
|
|
countEdits ThangType, done
|
|
|
|
patchesContributed: (done) ->
|
|
countPatchesByUsers {'status': 'accepted'}, 'stats.patchesContributed', done
|
|
|
|
patchesSubmitted: (done) ->
|
|
countPatchesByUsers {}, 'stats.patchesSubmitted', done
|
|
|
|
# The below need functions for filtering and are thus checked in memory
|
|
totalTranslationPatches: (done) ->
|
|
countPatchesByUsersInMemory {}, isTranslationPatch, 'stats.totalTranslationPatches', done
|
|
|
|
totalMiscPatches: (done) ->
|
|
countPatchesByUsersInMemory {}, isMiscPatch, 'stats.totalMiscPatches', done
|
|
|
|
articleMiscPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'article'}, isMiscPatch, User.statsMapping.misc.article, done
|
|
|
|
levelMiscPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'level'}, isMiscPatch, User.statsMapping.misc.level, done
|
|
|
|
levelComponentMiscPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'level_component'}, isMiscPatch, User.statsMapping.misc['level.component'], done
|
|
|
|
levelSystemMiscPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'level_system'}, isMiscPatch, User.statsMapping.misc['level.system'], done
|
|
|
|
thangTypeMiscPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'thang_type'}, isMiscPatch, User.statsMapping.misc['thang.type'], done
|
|
|
|
articleTranslationPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'article'}, isTranslationPatch, User.statsMapping.translations.article, done
|
|
|
|
levelTranslationPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'level'}, isTranslationPatch, User.statsMapping.translations.level, done
|
|
|
|
levelComponentTranslationPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'level_component'}, isTranslationPatch, User.statsMapping.translations['level.component'], done
|
|
|
|
levelSystemTranslationPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'level_system'}, isTranslationPatch, User.statsMapping.translations['level.system'], done
|
|
|
|
thangTypeTranslationPatches: (done) ->
|
|
countPatchesByUsersInMemory {'target.collection': 'thang_type'}, isTranslationPatch, User.statsMapping.translations['thang.type'], done
|
|
|
|
|
|
recalculateStats: (statName, done) =>
|
|
done new Error 'Recalculation handler not found' unless statName of @statRecalculators
|
|
@statRecalculators[statName] done
|
|
|
|
recalculate: (req, res, statName) ->
|
|
return @sendForbiddenError(res) unless req.user?.isAdmin()
|
|
log.debug 'recalculate'
|
|
return @sendNotFoundError(res) unless statName of @statRecalculators
|
|
@recalculateStats statName
|
|
@sendAccepted res, {}
|
|
|
|
module.exports = new UserHandler()
|