mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-01 07:40:22 -04:00
Added employer_list and activity tracking.
This commit is contained in:
parent
9c1213d3c4
commit
f3d18efa90
15 changed files with 301 additions and 21 deletions
app
locale
schemas
templates
views
server
|
@ -436,6 +436,7 @@
|
|||
av_entities_sub_title: "Entities"
|
||||
av_entities_users_url: "Users"
|
||||
av_entities_active_instances_url: "Active Instances"
|
||||
av_entities_employer_list_url: "Employer List"
|
||||
av_other_sub_title: "Other"
|
||||
av_other_debug_base_url: "Base (for debugging base.jade)"
|
||||
u_title: "User List"
|
||||
|
|
|
@ -113,8 +113,10 @@ UserSchema = c.object {},
|
|||
signedEmployerAgreement: c.object {},
|
||||
linkedinID: c.shortString {title:"LinkedInID", description: "The user's LinkedIn ID when they signed the contract."}
|
||||
date: c.date {title: "Date signed employer agreement"}
|
||||
data: c.object {description: "Cached LinkedIn data slurped from profile."}
|
||||
data: c.object {description: "Cached LinkedIn data slurped from profile.", additionalProperties: true}
|
||||
points: {type:'number'}
|
||||
activity: {type: 'object', description: 'Summary statistics about user activity', additionalProperties: c.activity}
|
||||
|
||||
|
||||
|
||||
c.extendBasicProperties UserSchema, 'user'
|
||||
|
|
|
@ -54,7 +54,7 @@ basicProps = (linkFragment) ->
|
|||
me.extendBasicProperties = (schema, linkFragment) ->
|
||||
schema.properties = {} unless schema.properties?
|
||||
_.extend(schema.properties, basicProps(linkFragment))
|
||||
|
||||
|
||||
# PATCHABLE
|
||||
|
||||
patchableProps = ->
|
||||
|
@ -65,7 +65,7 @@ patchableProps = ->
|
|||
allowPatches: { type: 'boolean' }
|
||||
watchers: me.array({title:'Watchers'},
|
||||
me.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]))
|
||||
|
||||
|
||||
me.extendPatchableProperties = (schema) ->
|
||||
schema.properties = {} unless schema.properties?
|
||||
_.extend(schema.properties, patchableProps())
|
||||
|
@ -170,3 +170,8 @@ me.FunctionArgumentSchema = me.object {
|
|||
title: "Default"
|
||||
description: "Default value of the argument. (Your code should set this.)"
|
||||
"default": null
|
||||
|
||||
me.activity = me.object {description: "Stats on an activity"},
|
||||
first: me.date()
|
||||
last: me.date()
|
||||
count: {type: 'integer', minimum: 0}
|
||||
|
|
|
@ -44,11 +44,6 @@ block content
|
|||
span(data-i18n='account_profile.not_featured') Not Featured
|
||||
if me.isAdmin() && !myProfile
|
||||
button.btn.edit-settings-button#enter-espionage-mode 007
|
||||
//if editing && myProfile
|
||||
// a.sample-profile(href="http://codecombat.com/images/pages/account/profile/sample_profile.png", target="_blank")
|
||||
// button.btn
|
||||
// i.icon-user
|
||||
// span(data-i18n="account_settings.sample_profile") See a sample profile
|
||||
|
||||
if profile && allowedToViewJobProfile
|
||||
div(class="job-profile-container" + (editing ? " editable-profile" : ""))
|
||||
|
|
|
@ -23,6 +23,8 @@ block content
|
|||
a(href="/admin/users", data-i18n="admin.av_entities_users_url") Users
|
||||
li
|
||||
a(href="/admin/level_sessions", data-i18n="admin.av_entities_active_instances_url") Active Instances
|
||||
li
|
||||
a(href="/admin/employer_list", data-i18n="admin.av_entities_employer_list_url") Employer List
|
||||
|
||||
h4(data-i18n="admin.av_other_sub_title") Other
|
||||
|
||||
|
|
65
app/templates/admin/employer_list.jade
Normal file
65
app/templates/admin/employer_list.jade
Normal file
|
@ -0,0 +1,65 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
|
||||
if !me.isAdmin()
|
||||
h1 Admin Only
|
||||
else
|
||||
h1(data-i18n="admin.av_entities_employer_list_url") Employer List
|
||||
|
||||
p
|
||||
| We currently have
|
||||
if employers.length
|
||||
| #{employers.length}
|
||||
else
|
||||
| ...
|
||||
| employers in the system.
|
||||
|
||||
if employers.length
|
||||
table.table.table-condensed.table-hover.table-responsive.tablesorter
|
||||
thead
|
||||
tr
|
||||
th(data-i18n="general.name") Name
|
||||
th Company
|
||||
th(data-i18n="general.email") Email
|
||||
th Logins
|
||||
th Candidates Viewed
|
||||
th Candidates Contacted
|
||||
th Signed Up
|
||||
|
||||
tbody
|
||||
for employer, index in employers
|
||||
- var activity = employer.get('activity') || {};
|
||||
- var linkedIn = employer.get('signedEmployerAgreement').data
|
||||
tr(data-employer-id=employer.id)
|
||||
td
|
||||
img(src=employer.getPhotoURL(50), height=50)
|
||||
p
|
||||
if employer.get('firstName')
|
||||
span= employer.get('firstName') + ' ' + employer.get('lastName')
|
||||
if employer.get('name')
|
||||
| -
|
||||
else if linkedIn.firstName
|
||||
span= linkedIn.firstName + ' ' + linkedIn.lastName
|
||||
if employer.get('name')
|
||||
| -
|
||||
if employer.get('name')
|
||||
span= employer.get('name')
|
||||
if !employer.get('firstName') && !linkedIn.firstName && !employer.get('name')
|
||||
| Anoner
|
||||
td
|
||||
a(href=employer.get('signedEmployerAgreement').data.publicProfileUrl)= employer.get('employerAt')
|
||||
td= employer.get('email')
|
||||
for a in ['login', 'view_candidate', 'contact_candidate']
|
||||
- var act = activity[a];
|
||||
if act
|
||||
td
|
||||
strong= act.count
|
||||
|
|
||||
br
|
||||
span= moment(activity.login.first).fromNow()
|
||||
br
|
||||
span= moment(activity.login.last).fromNow()
|
||||
else
|
||||
td 0
|
||||
td(data-employer-age=(new Date() - new Date(employer.get('signedEmployerAgreement').date)) / 86400 / 1000)= moment(employer.get('signedEmployerAgreement').date).fromNow()
|
|
@ -18,6 +18,11 @@ block content
|
|||
|
|
||||
a(href="http://blog.codecombat.com/multiplayer-programming-tournament", data-i18n="ladder.tournament_blurb_blog") on our blog
|
||||
| .
|
||||
p
|
||||
| At 5PM PDT today, we'll take a snapshot of submitted code and run an exhaustive pairwise ranking between the top N (at least 200) players. Winners will be sorted by
|
||||
code wins - losses
|
||||
| , and especially given deduplication on each team, final rankings may look significantly different than these preliminary rankings.
|
||||
| We will announce winners as soon as we can. Thanks for playing!
|
||||
|
||||
.sponsor-logos
|
||||
a(href="https://heapanalytics.com/")
|
||||
|
|
|
@ -59,6 +59,8 @@ module.exports = class ProfileView extends View
|
|||
@user.fetch()
|
||||
@listenTo @user, "sync", =>
|
||||
@render()
|
||||
$.post "/db/user/#{me.id}/track/view_candidate"
|
||||
$.post "/db/user/#{@userID}/track/viewed_by_employer" unless me.isAdmin()
|
||||
else
|
||||
@user = User.getByID(@userID)
|
||||
|
||||
|
@ -113,7 +115,7 @@ module.exports = class ProfileView extends View
|
|||
for position in p["positions"]["values"]
|
||||
workObj = {}
|
||||
descriptionMaxLength = workSchema.description.maxLength
|
||||
|
||||
|
||||
workObj.description = position.summary?.slice(0,descriptionMaxLength)
|
||||
workObj.description ?= ""
|
||||
if position.startDate?.year?
|
||||
|
|
160
app/views/admin/employer_list_view.coffee
Normal file
160
app/views/admin/employer_list_view.coffee
Normal file
|
@ -0,0 +1,160 @@
|
|||
View = require 'views/kinds/RootView'
|
||||
template = require 'templates/admin/employer_list'
|
||||
app = require 'application'
|
||||
User = require 'models/User'
|
||||
{me} = require 'lib/auth'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
ModelModal = require 'views/modal/model_modal'
|
||||
|
||||
class EmployersCollection extends CocoCollection
|
||||
url: '/db/user/x/employers'
|
||||
model: User
|
||||
|
||||
module.exports = class EmployersView extends View
|
||||
id: "employers-view"
|
||||
template: template
|
||||
|
||||
events:
|
||||
'click tbody tr td:first-child': 'onEmployerClicked'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@getEmployers()
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@sortTable() if @employers.models.length
|
||||
|
||||
getRenderData: ->
|
||||
ctx = super()
|
||||
ctx.employers = @employers.models
|
||||
ctx.moment = moment
|
||||
ctx
|
||||
|
||||
getEmployers: ->
|
||||
@employers = new EmployersCollection()
|
||||
@employers.fetch()
|
||||
# Re-render when we have fetched them, but don't wait and show a progress bar while loading.
|
||||
@listenToOnce @employers, 'all', => @render()
|
||||
|
||||
sortTable: ->
|
||||
# http://mottie.github.io/tablesorter/docs/example-widget-bootstrap-theme.html
|
||||
$.extend $.tablesorter.themes.bootstrap,
|
||||
# these classes are added to the table. To see other table classes available,
|
||||
# look here: http://twitter.github.com/bootstrap/base-css.html#tables
|
||||
table: "table table-bordered"
|
||||
caption: "caption"
|
||||
header: "bootstrap-header" # give the header a gradient background
|
||||
footerRow: ""
|
||||
footerCells: ""
|
||||
icons: "" # add "icon-white" to make them white; this icon class is added to the <i> in the header
|
||||
sortNone: "bootstrap-icon-unsorted"
|
||||
sortAsc: "icon-chevron-up" # glyphicon glyphicon-chevron-up" # we are still using v2 icons
|
||||
sortDesc: "icon-chevron-down" # glyphicon-chevron-down" # we are still using v2 icons
|
||||
active: "" # applied when column is sorted
|
||||
hover: "" # use custom css here - bootstrap class may not override it
|
||||
filterRow: "" # filter row class
|
||||
even: "" # odd row zebra striping
|
||||
odd: "" # even row zebra striping
|
||||
|
||||
|
||||
# e = exact text from cell
|
||||
# n = normalized value returned by the column parser
|
||||
# f = search filter input value
|
||||
# i = column index
|
||||
# $r = ???
|
||||
filterSelectExactMatch = (e, n, f, i, $r) -> e is f
|
||||
|
||||
# call the tablesorter plugin and apply the uitheme widget
|
||||
@$el.find(".tablesorter").tablesorter
|
||||
theme: "bootstrap"
|
||||
widthFixed: true
|
||||
headerTemplate: "{content} {icon}"
|
||||
textSorter:
|
||||
6: (a, b, direction, column, table) ->
|
||||
days = []
|
||||
for s in [a, b]
|
||||
n = parseInt s
|
||||
n = 0 unless _.isNumber n
|
||||
n = 1 if /^a/.test s
|
||||
for [duration, factor] in [
|
||||
[/second/i, 1 / (86400 * 1000)]
|
||||
[/minute/i, 1 / 1440]
|
||||
[/hour/i, 1 / 24]
|
||||
[/week/i, 7]
|
||||
[/month/i, 30.42]
|
||||
[/year/i, 365.2425]
|
||||
]
|
||||
if duration.test s
|
||||
n *= factor
|
||||
break
|
||||
if /^in /i.test s
|
||||
n *= -1
|
||||
days.push n
|
||||
days[0] - days[1]
|
||||
sortList: [[6, 0]]
|
||||
# widget code contained in the jquery.tablesorter.widgets.js file
|
||||
# use the zebra stripe widget if you plan on hiding any rows (filter widget)
|
||||
widgets: ["uitheme", "zebra", "filter"]
|
||||
widgetOptions:
|
||||
# using the default zebra striping class name, so it actually isn't included in the theme variable above
|
||||
# this is ONLY needed for bootstrap theming if you are using the filter widget, because rows are hidden
|
||||
zebra: ["even", "odd"]
|
||||
|
||||
# extra css class applied to the table row containing the filters & the inputs within that row
|
||||
filter_cssFilter: ""
|
||||
|
||||
# If there are child rows in the table (rows with class name from "cssChildRow" option)
|
||||
# and this option is true and a match is found anywhere in the child row, then it will make that row
|
||||
# visible; default is false
|
||||
filter_childRows: false
|
||||
|
||||
# if true, filters are collapsed initially, but can be revealed by hovering over the grey bar immediately
|
||||
# below the header row. Additionally, tabbing through the document will open the filter row when an input gets focus
|
||||
filter_hideFilters: false
|
||||
|
||||
# Set this option to false to make the searches case sensitive
|
||||
filter_ignoreCase: true
|
||||
|
||||
# jQuery selector string of an element used to reset the filters
|
||||
filter_reset: ".reset"
|
||||
|
||||
# Use the $.tablesorter.storage utility to save the most recent filters
|
||||
filter_saveFilters: true
|
||||
|
||||
# Delay in milliseconds before the filter widget starts searching; This option prevents searching for
|
||||
# every character while typing and should make searching large tables faster.
|
||||
filter_searchDelay: 150
|
||||
|
||||
# Set this option to true to use the filter to find text from the start of the column
|
||||
# So typing in "a" will find "albert" but not "frank", both have a's; default is false
|
||||
filter_startsWith: false
|
||||
|
||||
filter_functions:
|
||||
3:
|
||||
"0-1": (e, n, f, i, $r) -> parseInt(e) <= 1
|
||||
"2-5": (e, n, f, i, $r) -> 2 <= parseInt(e) <= 5
|
||||
"6+": (e, n, f, i, $r) -> 6 <= parseInt(e)
|
||||
4:
|
||||
"0-1": (e, n, f, i, $r) -> parseInt(e) <= 1
|
||||
"2-5": (e, n, f, i, $r) -> 2 <= parseInt(e) <= 5
|
||||
"6+": (e, n, f, i, $r) -> 6 <= parseInt(e)
|
||||
5:
|
||||
"0-1": (e, n, f, i, $r) -> parseInt(e) <= 1
|
||||
"2-5": (e, n, f, i, $r) -> 2 <= parseInt(e) <= 5
|
||||
"6+": (e, n, f, i, $r) -> 6 <= parseInt(e)
|
||||
6:
|
||||
"Last day": (e, n, f, i, $r) ->
|
||||
days = parseFloat $($r.find('td')[i]).data('employer-age')
|
||||
days <= 1
|
||||
"Last week": (e, n, f, i, $r) ->
|
||||
days = parseFloat $($r.find('td')[i]).data('employer-age')
|
||||
days <= 7
|
||||
"Last 4 weeks": (e, n, f, i, $r) ->
|
||||
days = parseFloat $($r.find('td')[i]).data('employer-age')
|
||||
days <= 28
|
||||
|
||||
onEmployerClicked: (e) ->
|
||||
return unless id = $(e.target).closest('tr').data('employer-id')
|
||||
employer = new User _id: id
|
||||
@openModalView new ModelModal models: [employer]
|
|
@ -181,9 +181,6 @@ module.exports = class EmployersView extends View
|
|||
7:
|
||||
"✓": filterSelectExactMatch
|
||||
"✗": filterSelectExactMatch
|
||||
8:
|
||||
"✓": filterSelectExactMatch
|
||||
"✗": filterSelectExactMatch
|
||||
|
||||
onCandidateClicked: (e) ->
|
||||
id = $(e.target).closest('tr').data('candidate-id')
|
||||
|
|
|
@ -33,3 +33,4 @@ module.exports = class ContactView extends View
|
|||
return forms.applyErrorsToForm @$el, res.errors unless res.valid
|
||||
window.tracker?.trackEvent 'Sent Feedback', message: contactMessage
|
||||
sendContactMessage contactMessage, @$el
|
||||
$.post "/db/user/#{me.id}/track/contact_codecombat"
|
||||
|
|
|
@ -39,3 +39,5 @@ module.exports = class JobProfileContactView extends ContactView
|
|||
contactMessage.message += '\n\n\n\n[CodeCombat says: please let us know if you end up accepting this job. Thanks!]'
|
||||
window.tracker?.trackEvent 'Sent Job Profile Message', message: contactMessage
|
||||
sendContactMessage contactMessage, @$el
|
||||
$.post "/db/user/#{me.id}/track/contact_candidate"
|
||||
$.post "/db/user/#{@options.recipientID}/track/contacted_by_employer" unless me.isAdmin()
|
||||
|
|
|
@ -61,8 +61,12 @@ module.exports.setup = (app) ->
|
|||
|
||||
req.logIn(user, (err) ->
|
||||
return next(err) if (err)
|
||||
res.send(UserHandler.formatEntity(req, req.user))
|
||||
return res.end()
|
||||
activity = req.user.trackActivity 'login', 1
|
||||
console.log "updating", activity
|
||||
user.update {activity: activity}, (err) ->
|
||||
return next(err) if (err)
|
||||
res.send(UserHandler.formatEntity(req, req.user))
|
||||
return res.end()
|
||||
)
|
||||
)(req, res, next)
|
||||
)
|
||||
|
@ -134,12 +138,12 @@ module.exports.setup = (app) ->
|
|||
|
||||
emails = _.clone(user.get('emails')) or {}
|
||||
msg = ''
|
||||
|
||||
|
||||
if req.query.recruitNotes
|
||||
emails.recruitNotes ?= {}
|
||||
emails.recruitNotes.enabled = false
|
||||
msg = "Unsubscribed #{req.query.email} from recruiting emails."
|
||||
|
||||
|
||||
else
|
||||
msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!"
|
||||
emailSettings.enabled = false for emailSettings in _.values(emails)
|
||||
|
@ -147,7 +151,7 @@ module.exports.setup = (app) ->
|
|||
emails.generalNews.enabled = false
|
||||
emails.anyNotes ?= {}
|
||||
emails.anyNotes.enabled = false
|
||||
|
||||
|
||||
user.update {$set: {emails: emails}}, {}, =>
|
||||
return errors.serverError res, 'Database failure.' if err
|
||||
res.send msg + "<p><a href='/account/settings'>Account settings</a></p>"
|
||||
|
@ -172,7 +176,7 @@ module.exports.makeNewUser = makeNewUser = (req) ->
|
|||
user = new User({anonymous:true})
|
||||
user.set 'testGroupNumber', Math.floor(Math.random() * 256) # also in app/lib/auth
|
||||
user.set 'preferredLanguage', languages.languageCodeFromAcceptedLanguages req.acceptedLanguages
|
||||
|
||||
|
||||
createMailOptions = (receiver, password) ->
|
||||
# TODO: use email templates here
|
||||
options =
|
||||
|
@ -181,4 +185,3 @@ createMailOptions = (receiver, password) ->
|
|||
replyTo: config.mail.username
|
||||
subject: "[CodeCombat] Password Reset"
|
||||
text: "You can log into your account with: #{password}"
|
||||
|
||||
|
|
|
@ -29,6 +29,17 @@ UserSchema.methods.isAdmin = ->
|
|||
p = @get('permissions')
|
||||
return p and 'admin' in p
|
||||
|
||||
UserSchema.methods.trackActivity = (activityName, increment) ->
|
||||
now = new Date()
|
||||
increment ?= parseInt increment or 1
|
||||
increment = Math.max increment, 0
|
||||
activity = @get('activity') ? {}
|
||||
activity[activityName] ?= {first: now, count: 0}
|
||||
activity[activityName].count += increment
|
||||
activity[activityName].last = now
|
||||
@set 'activity', activity
|
||||
activity
|
||||
|
||||
emailNameMap =
|
||||
generalNews: 'announcement'
|
||||
adventurerNews: 'tester'
|
||||
|
|
|
@ -48,7 +48,7 @@ UserHandler = class UserHandler extends Handler
|
|||
delete obj[prop] for prop in serverProperties
|
||||
includePrivates = req.user and (req.user.isAdmin() or req.user._id.equals(document._id))
|
||||
delete obj[prop] for prop in privateProperties unless includePrivates
|
||||
includeCandidate = includePrivates or (obj.jobProfileApproved and req.user and ('employer' in (req.user.get('permissions') ? [])) and @employerCanViewCandidate req.user, obj)
|
||||
includeCandidate = 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 candidateProperties unless includeCandidate
|
||||
return obj
|
||||
|
||||
|
@ -191,9 +191,11 @@ UserHandler = class UserHandler extends Handler
|
|||
return @nameToID(req, res, args[0]) if args[1] is 'nameToID'
|
||||
return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions'
|
||||
return @getCandidates(req, res) if args[1] is 'candidates'
|
||||
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 @trackActivity(req, res, args[0], args[2], args[3]) if args[1] is 'track' and args[2]
|
||||
return @sendNotFoundError(res)
|
||||
super(arguments...)
|
||||
|
||||
|
@ -249,6 +251,25 @@ UserHandler = class UserHandler extends Handler
|
|||
doc.save()
|
||||
@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', 'messaged_by_employer'])
|
||||
return @sendUnauthorizedError 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.")
|
||||
|
@ -281,7 +302,7 @@ UserHandler = class UserHandler extends Handler
|
|||
query = {'jobProfile.updated': {$gt: since}}
|
||||
#query.jobProfileApproved = true unless req.user.isAdmin() # We split into featured and other now.
|
||||
query['jobProfile.active'] = true unless req.user.isAdmin()
|
||||
selection = 'jobProfile jobProfileApproved'
|
||||
selection = 'jobProfile jobProfileApproved photoURL'
|
||||
selection += ' email' if authorized
|
||||
User.find(query).select(selection).exec (err, documents) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
|
@ -309,6 +330,14 @@ UserHandler = class UserHandler extends Handler
|
|||
return false if job.employer?.toLowerCase() is employer.get('employerAt')?.toLowerCase()
|
||||
true
|
||||
|
||||
getEmployers: (req, res) ->
|
||||
return @sendUnauthorizedError(res) unless req.user.isAdmin()
|
||||
query = {employerAt: {$exists: true}}
|
||||
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"
|
||||
|
|
Loading…
Add table
Reference in a new issue