Merge branch 'master' into production
This commit is contained in:
commit
0527ca7229
19 changed files with 308 additions and 30 deletions
|
@ -436,6 +436,7 @@
|
||||||
av_entities_sub_title: "Entities"
|
av_entities_sub_title: "Entities"
|
||||||
av_entities_users_url: "Users"
|
av_entities_users_url: "Users"
|
||||||
av_entities_active_instances_url: "Active Instances"
|
av_entities_active_instances_url: "Active Instances"
|
||||||
|
av_entities_employer_list_url: "Employer List"
|
||||||
av_other_sub_title: "Other"
|
av_other_sub_title: "Other"
|
||||||
av_other_debug_base_url: "Base (for debugging base.jade)"
|
av_other_debug_base_url: "Base (for debugging base.jade)"
|
||||||
u_title: "User List"
|
u_title: "User List"
|
||||||
|
|
|
@ -113,8 +113,10 @@ UserSchema = c.object {},
|
||||||
signedEmployerAgreement: c.object {},
|
signedEmployerAgreement: c.object {},
|
||||||
linkedinID: c.shortString {title:"LinkedInID", description: "The user's LinkedIn ID when they signed the contract."}
|
linkedinID: c.shortString {title:"LinkedInID", description: "The user's LinkedIn ID when they signed the contract."}
|
||||||
date: c.date {title: "Date signed employer agreement"}
|
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'}
|
points: {type:'number'}
|
||||||
|
activity: {type: 'object', description: 'Summary statistics about user activity', additionalProperties: c.activity}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
c.extendBasicProperties UserSchema, 'user'
|
c.extendBasicProperties UserSchema, 'user'
|
||||||
|
|
|
@ -170,3 +170,8 @@ me.FunctionArgumentSchema = me.object {
|
||||||
title: "Default"
|
title: "Default"
|
||||||
description: "Default value of the argument. (Your code should set this.)"
|
description: "Default value of the argument. (Your code should set this.)"
|
||||||
"default": null
|
"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
|
span(data-i18n='account_profile.not_featured') Not Featured
|
||||||
if me.isAdmin() && !myProfile
|
if me.isAdmin() && !myProfile
|
||||||
button.btn.edit-settings-button#enter-espionage-mode 007
|
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
|
if profile && allowedToViewJobProfile
|
||||||
div(class="job-profile-container" + (editing ? " editable-profile" : ""))
|
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
|
a(href="/admin/users", data-i18n="admin.av_entities_users_url") Users
|
||||||
li
|
li
|
||||||
a(href="/admin/level_sessions", data-i18n="admin.av_entities_active_instances_url") Active Instances
|
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
|
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,16 @@ block content
|
||||||
|
|
|
|
||||||
a(href="http://blog.codecombat.com/multiplayer-programming-tournament", data-i18n="ladder.tournament_blurb_blog") on our blog
|
a(href="http://blog.codecombat.com/multiplayer-programming-tournament", data-i18n="ladder.tournament_blurb_blog") on our blog
|
||||||
| .
|
| .
|
||||||
|
p
|
||||||
|
strong Tournament ended!
|
||||||
|
| At 5PM PDT, we took a snapshot of submitted code and are running an exhaustive pairwise ranking between all 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!
|
||||||
|
p
|
||||||
|
| Want to commiserate? Head over to
|
||||||
|
a(href="http://discourse.codecombat.com/") the forum
|
||||||
|
| and discuss your strategies, your triumphs, and your turmoils.
|
||||||
|
|
||||||
.sponsor-logos
|
.sponsor-logos
|
||||||
a(href="https://heapanalytics.com/")
|
a(href="https://heapanalytics.com/")
|
||||||
|
|
|
@ -59,6 +59,8 @@ module.exports = class ProfileView extends View
|
||||||
@user.fetch()
|
@user.fetch()
|
||||||
@listenTo @user, "sync", =>
|
@listenTo @user, "sync", =>
|
||||||
@render()
|
@render()
|
||||||
|
$.post "/db/user/#{me.id}/track/view_candidate"
|
||||||
|
$.post "/db/user/#{@userID}/track/viewed_by_employer" unless me.isAdmin()
|
||||||
else
|
else
|
||||||
@user = User.getByID(@userID)
|
@user = User.getByID(@userID)
|
||||||
|
|
||||||
|
|
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:
|
7:
|
||||||
"✓": filterSelectExactMatch
|
"✓": filterSelectExactMatch
|
||||||
"✗": filterSelectExactMatch
|
"✗": filterSelectExactMatch
|
||||||
8:
|
|
||||||
"✓": filterSelectExactMatch
|
|
||||||
"✗": filterSelectExactMatch
|
|
||||||
|
|
||||||
onCandidateClicked: (e) ->
|
onCandidateClicked: (e) ->
|
||||||
id = $(e.target).closest('tr').data('candidate-id')
|
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
|
return forms.applyErrorsToForm @$el, res.errors unless res.valid
|
||||||
window.tracker?.trackEvent 'Sent Feedback', message: contactMessage
|
window.tracker?.trackEvent 'Sent Feedback', message: contactMessage
|
||||||
sendContactMessage contactMessage, @$el
|
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!]'
|
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
|
window.tracker?.trackEvent 'Sent Job Profile Message', message: contactMessage
|
||||||
sendContactMessage contactMessage, @$el
|
sendContactMessage contactMessage, @$el
|
||||||
|
$.post "/db/user/#{me.id}/track/contact_candidate"
|
||||||
|
$.post "/db/user/#{@options.recipientID}/track/contacted_by_employer" unless me.isAdmin()
|
||||||
|
|
|
@ -3,14 +3,10 @@ startsWith = (string, substring) ->
|
||||||
string.lastIndexOf(substring, 0) is 0
|
string.lastIndexOf(substring, 0) is 0
|
||||||
|
|
||||||
exports.config =
|
exports.config =
|
||||||
server:
|
|
||||||
path: 'server.coffee'
|
|
||||||
paths:
|
paths:
|
||||||
'public': 'public'
|
'public': 'public'
|
||||||
conventions:
|
conventions:
|
||||||
ignored: (path) -> startsWith(sysPath.basename(path), '_')
|
ignored: (path) -> startsWith(sysPath.basename(path), '_')
|
||||||
workers:
|
|
||||||
enabled: false # turned out to be much, much slower than without workers
|
|
||||||
sourceMaps: true
|
sourceMaps: true
|
||||||
files:
|
files:
|
||||||
javascripts:
|
javascripts:
|
||||||
|
@ -55,7 +51,6 @@ exports.config =
|
||||||
'vendor/scripts/movieclip-NEXT.min.js'
|
'vendor/scripts/movieclip-NEXT.min.js'
|
||||||
# Validated Backbone Mediator dependencies
|
# Validated Backbone Mediator dependencies
|
||||||
'bower_components/tv4/tv4.js'
|
'bower_components/tv4/tv4.js'
|
||||||
|
|
||||||
# Aether before box2d for some strange Object.defineProperty thing
|
# Aether before box2d for some strange Object.defineProperty thing
|
||||||
'bower_components/aether/build/aether.js'
|
'bower_components/aether/build/aether.js'
|
||||||
'bower_components/d3/d3.min.js'
|
'bower_components/d3/d3.min.js'
|
||||||
|
@ -77,7 +72,7 @@ exports.config =
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
autoReload:
|
autoReload:
|
||||||
delay: 300 # for race conditions, particularly waiting for onCompile to do its thing
|
delay: 300
|
||||||
coffeelint:
|
coffeelint:
|
||||||
pattern: /^app\/.*\.coffee$/
|
pattern: /^app\/.*\.coffee$/
|
||||||
options:
|
options:
|
||||||
|
|
|
@ -12,7 +12,6 @@ LevelComponentSchema.plugin plugins.PermissionsPlugin
|
||||||
LevelComponentSchema.plugin plugins.VersionedPlugin
|
LevelComponentSchema.plugin plugins.VersionedPlugin
|
||||||
LevelComponentSchema.plugin plugins.SearchablePlugin, {searchable: ['name', 'description', 'system']}
|
LevelComponentSchema.plugin plugins.SearchablePlugin, {searchable: ['name', 'description', 'system']}
|
||||||
LevelComponentSchema.plugin plugins.PatchablePlugin
|
LevelComponentSchema.plugin plugins.PatchablePlugin
|
||||||
LevelComponentSchema.plugin plugins.MigrationPlugin, {'language': 'codeLanguage'}
|
|
||||||
|
|
||||||
LevelComponentSchema.pre 'init', (next) ->
|
LevelComponentSchema.pre 'init', (next) ->
|
||||||
return next() unless jsonschema.properties?
|
return next() unless jsonschema.properties?
|
||||||
|
|
|
@ -11,7 +11,6 @@ LevelSystemSchema.plugin(plugins.PermissionsPlugin)
|
||||||
LevelSystemSchema.plugin(plugins.VersionedPlugin)
|
LevelSystemSchema.plugin(plugins.VersionedPlugin)
|
||||||
LevelSystemSchema.plugin(plugins.SearchablePlugin, {searchable: ['name', 'description']})
|
LevelSystemSchema.plugin(plugins.SearchablePlugin, {searchable: ['name', 'description']})
|
||||||
LevelSystemSchema.plugin(plugins.PatchablePlugin)
|
LevelSystemSchema.plugin(plugins.PatchablePlugin)
|
||||||
LevelSystemSchema.plugin plugins.MigrationPlugin, {'language': 'codeLanguage'}
|
|
||||||
|
|
||||||
LevelSystemSchema.pre 'init', (next) ->
|
LevelSystemSchema.pre 'init', (next) ->
|
||||||
return next() unless jsonschema.properties?
|
return next() unless jsonschema.properties?
|
||||||
|
|
|
@ -9,7 +9,7 @@ module.exports.MigrationPlugin = (schema, migrations) ->
|
||||||
# 1. Change the schema and the client/server logic to use the new name
|
# 1. Change the schema and the client/server logic to use the new name
|
||||||
# 2. Add this plugin to the target models, passing in a dictionary of old/new names.
|
# 2. Add this plugin to the target models, passing in a dictionary of old/new names.
|
||||||
# 3. Check that tests still run, deploy to production.
|
# 3. Check that tests still run, deploy to production.
|
||||||
# 4. Run db.<collection>.update({}, { $rename: {'<oldname>':'<newname>'} }) on the server
|
# 4. Run db.<collection>.update({}, { $rename: {'<oldname>':'<newname>'} }, { multi: true }) on the server
|
||||||
# 5. Remove the names you added to the migrations dictionaries for the next deploy
|
# 5. Remove the names you added to the migrations dictionaries for the next deploy
|
||||||
|
|
||||||
schema.post 'init', ->
|
schema.post 'init', ->
|
||||||
|
|
|
@ -60,6 +60,10 @@ module.exports.setup = (app) ->
|
||||||
return errors.unauthorized(res, [{message:info.message, property:info.property}])
|
return errors.unauthorized(res, [{message:info.message, property:info.property}])
|
||||||
|
|
||||||
req.logIn(user, (err) ->
|
req.logIn(user, (err) ->
|
||||||
|
return next(err) if (err)
|
||||||
|
activity = req.user.trackActivity 'login', 1
|
||||||
|
console.log "updating", activity
|
||||||
|
user.update {activity: activity}, (err) ->
|
||||||
return next(err) if (err)
|
return next(err) if (err)
|
||||||
res.send(UserHandler.formatEntity(req, req.user))
|
res.send(UserHandler.formatEntity(req, req.user))
|
||||||
return res.end()
|
return res.end()
|
||||||
|
@ -181,4 +185,3 @@ createMailOptions = (receiver, password) ->
|
||||||
replyTo: config.mail.username
|
replyTo: config.mail.username
|
||||||
subject: "[CodeCombat] Password Reset"
|
subject: "[CodeCombat] Password Reset"
|
||||||
text: "You can log into your account with: #{password}"
|
text: "You can log into your account with: #{password}"
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,17 @@ UserSchema.methods.isAdmin = ->
|
||||||
p = @get('permissions')
|
p = @get('permissions')
|
||||||
return p and 'admin' in p
|
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 =
|
emailNameMap =
|
||||||
generalNews: 'announcement'
|
generalNews: 'announcement'
|
||||||
adventurerNews: 'tester'
|
adventurerNews: 'tester'
|
||||||
|
|
|
@ -48,7 +48,7 @@ UserHandler = class UserHandler extends Handler
|
||||||
delete obj[prop] for prop in serverProperties
|
delete obj[prop] for prop in serverProperties
|
||||||
includePrivates = req.user and (req.user.isAdmin() or req.user._id.equals(document._id))
|
includePrivates = req.user and (req.user.isAdmin() or req.user._id.equals(document._id))
|
||||||
delete obj[prop] for prop in privateProperties unless includePrivates
|
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
|
delete obj[prop] for prop in candidateProperties unless includeCandidate
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
@ -191,9 +191,11 @@ UserHandler = class UserHandler extends Handler
|
||||||
return @nameToID(req, res, args[0]) if args[1] is 'nameToID'
|
return @nameToID(req, res, args[0]) if args[1] is 'nameToID'
|
||||||
return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions'
|
return @getLevelSessions(req, res, args[0]) if args[1] is 'level.sessions'
|
||||||
return @getCandidates(req, res) if args[1] is 'candidates'
|
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 @getSimulatorLeaderboard(req, res, args[0]) if args[1] is 'simulatorLeaderboard'
|
||||||
return @getMySimulatorLeaderboardRank(req, res, args[0]) if args[1] is 'simulator_leaderboard_rank'
|
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 @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)
|
return @sendNotFoundError(res)
|
||||||
super(arguments...)
|
super(arguments...)
|
||||||
|
|
||||||
|
@ -249,6 +251,25 @@ UserHandler = class UserHandler extends Handler
|
||||||
doc.save()
|
doc.save()
|
||||||
@sendSuccess(res, cleandocs)
|
@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) ->
|
agreeToEmployerAgreement: (req, res) ->
|
||||||
userIsAnonymous = req.user?.get('anonymous')
|
userIsAnonymous = req.user?.get('anonymous')
|
||||||
if userIsAnonymous then return errors.unauthorized(res, "You need to be logged in to agree to the employer agreeement.")
|
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 = {'jobProfile.updated': {$gt: since}}
|
||||||
#query.jobProfileApproved = true unless req.user.isAdmin() # We split into featured and other now.
|
#query.jobProfileApproved = true unless req.user.isAdmin() # We split into featured and other now.
|
||||||
query['jobProfile.active'] = true unless req.user.isAdmin()
|
query['jobProfile.active'] = true unless req.user.isAdmin()
|
||||||
selection = 'jobProfile jobProfileApproved'
|
selection = 'jobProfile jobProfileApproved photoURL'
|
||||||
selection += ' email' if authorized
|
selection += ' email' if authorized
|
||||||
User.find(query).select(selection).exec (err, documents) =>
|
User.find(query).select(selection).exec (err, documents) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
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()
|
return false if job.employer?.toLowerCase() is employer.get('employerAt')?.toLowerCase()
|
||||||
true
|
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) ->
|
buildGravatarURL: (user, size, fallback) ->
|
||||||
emailHash = @buildEmailHash user
|
emailHash = @buildEmailHash user
|
||||||
fallback ?= "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png"
|
fallback ?= "http://codecombat.com/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png"
|
||||||
|
|
Reference in a new issue